How PGSQL can facilitate the data processing workflow
Parfait Gasana
Data Analyst, Winston & Strawn
## Libraries
library(DBI)
Loading required package: methods
library(RPostgreSQL)
library(microbenchmark)
library(scales)
library(ggplot2)
options(repr.plot.width=15, repr.plot.height=5)
options(scipen=999)
Import CSV Data
Often one of the largest time and resource-intensive steps for data analysts
Requires disk space on shared network or local drives
Multiple files usually unmanaged across versions and teams
csv_import <- function() {
setwd("~/Documents/PGSQL")
bus_month <- read.csv('CTA_-_Ridership_-_Bus_Routes_-_Monthly_Day-Type_Averages___Totals.csv')
bus_rides <- read.csv('CTA_-_Ridership_-_Bus_Routes_-_Daily_Totals_by_Route.csv')
rail_stops <- read.csv('CTA_-_System_Information_-_List_of__L__Stops.csv')
rail_rides <- read.csv('CTA_-_Ridership_-__L__Station_Entries_-_Daily_Totals.csv')
}
Import SQL Data
Centralized data access locally or remotely
Relational model ensures no redundancy
Secured and robust data storage
Available backup with no need of local computers
Table and set relations seamless integrates into datasets
atomic columns and diverse rowset
sql_import <- function() {
conn <- dbConnect(RPostgreSQL::PostgreSQL(), host="10.0.0.220", dbname="cta",
user="ctadba", password="cta18!", port=5432)
bus_month <- dbGetQuery(conn, "SELECT * FROM bus_month")
bus_rides <- dbGetQuery(conn, "SELECT * FROM bus_rides")
rail_stops <- dbGetQuery(conn, "SELECT * FROM rail_stops")
rail_rides <- dbGetQuery(conn, "SELECT * FROM rail_rides")
dbDisconnect(conn)
}
summary(microbenchmark(csv_import))
summary(microbenchmark(sql_import))
View CSV Data
Adjusting different levels and reshaping for analytical purposes
bus_month_csv <- read.csv('CTA_-_Ridership_-_Bus_Routes_-_Monthly_Day-Type_Averages___Totals.csv')
head(bus_month_csv)
bus_rides_csv <- read.csv('CTA_-_Ridership_-_Bus_Routes_-_Daily_Totals_by_Route.csv')
head(bus_rides_csv)
rail_stops_csv <- read.csv('CTA_-_System_Information_-_List_of__L__Stops.csv')
names(rail_stops_csv) <- tolower(names(rail_stops_csv))
head(rail_stops_csv)
rail_rides_csv <- read.csv('CTA_-_Ridership_-__L__Station_Entries_-_Daily_Totals.csv')
head(rail_rides_csv)
bus_month_csv$Month_Beginning <- as.Date(bus_month_csv$Month_Beginning, format="%m/%d/%Y", origin="1970-01-01")
bus_rides_csv$date <- as.Date(bus_rides_csv$date, format="%m/%d/%Y", origin="1970-01-01")
rail_rides_csv$date <- as.Date(rail_rides_csv$date, format="%m/%d/%Y", origin="1970-01-01")
View SQL Data
Relational databases are planned systems
Constraints and referential integrity ensures stability and relationships
Query language provides expression, reliablibility and versatility
conn <- dbConnect(RPostgreSQL::PostgreSQL(), host="10.0.0.220", dbname="cta",
user="ctadba", password="cta18!", port=5432)
bus_month_sql <- dbGetQuery(conn, "SELECT * FROM bus_month")
head(bus_month_sql)
bus_rides_sql <- dbGetQuery(conn, "SELECT * FROM bus_rides")
head(bus_rides_sql)
rail_stations_sql <- dbGetQuery(conn, "SELECT * FROM rail_stations")
head(rail_stations_sql)
rail_rides_sql <- dbGetQuery(conn, "SELECT * FROM rail_rides")
head(rail_rides_sql)
Aggregate CSV Data
Complex processes require long piping/chaining of objects and methods
Language lacks set-based (i.e., relations, join, union) framework
# MERGE
agg_csv <- merge(unique(bus_month_csv[c("route", "routename")]), bus_rides_csv, by="route")
# AGGREGATE
agg_csv <- do.call(data.frame,
aggregate(rides ~ route + routename, agg_csv,
function(x) c(count=length(x), sum=sum(x), mean=mean(x),
median=median(x), min=min(x), max=max(x))))
# ORDER
agg_csv <- with(agg_csv, agg_csv[order(-rides.sum),])
agg_csv
Top 5 Bus Routes
# MERGE
agg_csv <- merge(subset(unique(bus_month_csv[c("route", "routename")]),
routename %in% c("79th", "Ashland", "Chicago", "Western", "Cottage Grove")),
transform(bus_rides_csv, year=format(date, "%Y")),
by="route")
# AGGREGATE
agg_csv <- do.call(data.frame,
aggregate(rides ~ routename + year, agg_csv,
function(x) c(count=length(x), sum=sum(x), mean=mean(x),
median=median(x), min=min(x), max=max(x))))
# ORDER
agg_csv <- with(agg_csv, agg_csv[order(routename, year),])
agg_csv
Graph CSV Data
seabornPalette <- c("#4c72b0","#55a868","#c44e52","#8172b2","#ccb974","#64b5cd","#4c72b0","#55a868",
"#c44e52","#8172b2","#ccb974","#64b5cd","#4c72b0","#55a868","#c44e52","#8172b2",
"#ccb974","#64b5cd","#4c72b0","#55a868","#c44e52","#8172b2","#ccb974","#64b5cd",
"#4c72b0","#55a868","#c44e52","#8172b2","#ccb974","#64b5cd","#4c72b0","#55a868",
"#c44e52","#8172b2","#ccb974","#64b5cd","#4c72b0","#55a868","#c44e52","#8172b2",
"#ccb974","#64b5cd","#4c72b0","#55a868","#c44e52","#8172b2","#ccb974","#64b5cd",
"#4c72b0","#55a868","#c44e52","#8172b2","#ccb974","#64b5cd","#4c72b0","#55a868",
"#c44e52","#8172b2","#ccb974","#64b5cd")
ggplot(agg_csv, aes(year, rides.sum, fill=routename)) + geom_col(position = "dodge") +
labs(title="Top 5 CTA 'L' Bus Routes", x="Year", y="Rides") +
scale_y_continuous(expand = c(0, 0), label=comma) +
scale_fill_manual(values = seabornPalette) +
theme(legend.position="bottom",
plot.title = element_text(hjust=0.5, size=18),
axis.text.x = element_text(angle=0, hjust=0.5))

Aggregate SQL Data
Clear, compact declarative language with portability
Processing with virtual tables occurs behind the scene
Set-based framework facilitates blockwise, vectorized process
sql <- 'SELECT rt.route_name, COUNT(rd.rides) AS "count",
SUM(rd.rides) AS "sum",
AVG(rd.rides) AS "mean",
MEDIAN(rd.rides) AS "median",
MIN(rd.rides) AS "min",
MAX(rd.rides) AS "max"
FROM bus_routes rt
INNER JOIN bus_rides rd ON rt.route_id = rd.route_id
GROUP BY rt.route_name
ORDER BY SUM(rd.rides) DESC'
agg_sql <- dbGetQuery(conn, sql)
agg_sql
sql <- 'SELECT rt.route_name, DATE_PART(\'year\', rd.ride_date)::integer AS "year",
COUNT(rd.rides) AS "count",
SUM(rd.rides) AS "sum",
AVG(rd.rides) AS "mean",
MEDIAN(rd.rides) AS "median",
MIN(rd.rides) AS "min",
MAX(rd.rides) AS "max"
FROM bus_routes rt
INNER JOIN bus_rides rd ON rt.route_id = rd.route_id
WHERE rt.route_name IN (\'79th\', \'Ashland\', \'Chicago\', \'Western\', \'Cottage Grove\')
GROUP BY rt.route_name, DATE_PART(\'year\', rd.ride_date)::integer
ORDER BY rt.route_name, DATE_PART(\'year\', rd.ride_date)::integer'
agg_sql <- dbGetQuery(conn, sql)
agg_sql
Graph SQL Data
ggplot(agg_sql, aes(year, sum, color=route_name)) +
geom_line(stat="identity") + geom_point(stat="identity") +
labs(title="Top 5 CTA 'L' Bus Routes", x="Year", y="Rides") +
scale_x_continuous("year", breaks=unique(agg_sql$year)) +
scale_y_continuous(expand = c(0, 0), label=comma) +
scale_color_manual(values = seabornPalette) +
theme(legend.position="bottom",
plot.title = element_text(hjust=0.5, size=18),
axis.text.x = element_text(angle=0, hjust=0.5))

CSV Data Diagnostics
Imperative nature of processing
Dense, nested calls for layered steps
Limited to application layer
# TRANSFORM
agg_csv <- transform(rail_rides_csv, year=format(date, "%Y"))
# AGGREGATE
agg_csv <- do.call(data.frame,
aggregate(rides ~ station_id + stationname + year, agg_csv,
function(x) c(count=length(x), sum=sum(x), mean=mean(x),
median=median(x), min=min(x), max=max(x))))
# ORDER
agg_csv <- with(agg_csv, agg_csv[order(-rides.sum),])
agg_csv
# RESHAPE
rail_lines <- c("red", "blue", "green", "brown", "purple", "purple_exp", "yellow", "pink", "orange")
rail_long <- reshape(setNames(rail_stops_csv[c("map_id", "station_name", "location", "red", "blue", "g",
"brn", "p", "pexp", "y", "pnk", "o")],
c("station_id", "station_name", "location", rail_lines)),
varying = rail_lines, v.names = "value",
timevar = "rail_line", times = rail_lines,
new.row.names = 1:10000, direction = "long")
# SUBSET
rail_long <- unique(subset(rail_long, value=='true')[c("station_id", "station_name", "location", "rail_line")])
# ORDER
rail_long <- with(rail_long, rail_long[order(station_id, rail_line),])
row.names(rail_long) <- NULL
rail_long
merge_rail <- merge(agg_csv, rail_long, by="station_id")
merge_rail$rides.total <- merge_rail$rides.sum / with(merge_rail, ave(station_id, station_id, year, FUN=length))
merge_rail
agg_csv <- aggregate(rides.total ~ year + rail_line, merge_rail, sum)
agg_csv <- with(agg_csv, agg_csv[order(rail_line, year),])
row.names(agg_csv) <- NULL
agg_csv
SQL Data Diagnostics
Complex processing still readable and maintainable
CTEs clearly show underlying tables and views without helper objects
Window functions allow useful inline calculations
Conditional aggregations clearly show reshaped data
Seamless casting and conversion of types with :: operator
sql <- 'WITH station_agg AS
(SELECT DATE_PART(\'year\', r.ride_date)::integer AS "year",
r.station_id,
r.station_name,
COUNT(r.rides)::numeric(20,5) AS "count",
SUM(r.rides)::numeric(20,5) AS "sum",
AVG(r.rides)::numeric(20,5) AS "mean",
MEDIAN(r.rides)::numeric(20,5) AS "median",
MIN(r.rides)::numeric(20,5) AS "min",
MAX(r.rides)::numeric(20,5) AS "max"
FROM rail_rides r
GROUP BY DATE_PART(\'year\', r.ride_date),
r.station_id,
r.station_name
),
merge_rail AS
(SELECT s.*,
r.rail_line,
(s."sum" / COUNT(*) OVER(PARTITION BY s.station_id, "year")) AS rail_total
FROM station_agg s
INNER JOIN rail_stations r ON s.station_id = r.station_id
)
SELECT m."year", m.rail_line, SUM(m.rail_total) AS rail_total
FROM merge_rail m
GROUP BY m."year", m.rail_line
ORDER BY m.rail_line, m."year"'
agg_sql <- dbGetQuery(conn, sql)
agg_sql
cta_palette <- c(blue="#00A1DE", brown="#62361B", green="#009B3A", orange="#F9461C", pink="#E27EA6",
purple="#522398", purple_exp="#8059BA", red="#C60C30", yellow="#F9E300")
ggplot(subset(agg_sql, year > 2012), aes(year, rail_total, fill=rail_line)) + geom_col(position = "dodge") +
labs(title="CTA System 'L' Lines - Total Rides By Year", x="Year", y="Rides") +
scale_x_continuous(expand = c(0,0), "year", breaks=unique(agg_sql$year)) +
scale_y_continuous(expand = c(0, 0), label=comma) +
scale_fill_manual(values = cta_palette) + guides(fill=guide_legend("Rail Lines", nrow=1)) +
theme(legend.position="bottom",
plot.title = element_text(hjust=0.5, size=18),
axis.text.x = element_text(angle=0, hjust=0.5))

Distribution
sql <- 'SELECT r.station_id, r.ride_date, r.station_name, s.rail_line, r.rides,
(r.rides / COUNT(*) OVER(PARTITION BY s.station_id, r.ride_date)) AS rides_total
FROM rail_rides r
INNER JOIN rail_stations s ON s.station_id = r.station_id'
hist_long <- dbGetQuery(conn, sql)
hist_long
ggplot(hist_long, aes(x=rides_total, fill=rail_line)) +
geom_histogram(data=subset(hist_long, rail_line == 'red'), bins=100) +
geom_histogram(data=subset(hist_long, rail_line == 'blue'), bins=100) +
geom_histogram(data=subset(hist_long, rail_line == 'brown'), bins=100) +
geom_histogram(data=subset(hist_long, rail_line == 'green'), bins=100) +
geom_histogram(data=subset(hist_long, rail_line == 'orange'), bins=100) +
scale_x_continuous(expand = c(0, 0)) +
scale_y_continuous(expand = c(0, 0), lim=c(0,40000), label=comma) +
scale_fill_manual(values = c(red = "#C60C30", blue = "#00A1DE", brown = "#62361B",
green = "#009B3A", orange = "#F9461C")) +
labs(title="CTA Ridership Distribution By Rail Line", fill="Rail Line") +
theme(plot.title = element_text(hjust=0.5, size=18))

ggplot(transform(hist_long, year = format(ride_date, '%Y')),
aes(x=year, y=rides_total, fill=year)) +
geom_boxplot() + guides(fill=FALSE) +
scale_y_continuous(expand = c(0, 0), lim=c(0,40000), label=comma) +
labs(title="CTA Ridership Boxplot By Year") +
theme(plot.title = element_text(hjust=0.5, size=18))

ggplot(hist_long, aes(x=rail_line, y=rides_total, fill=rail_line)) +
geom_boxplot() + guides(fill=FALSE) +
scale_fill_manual(values = cta_palette) +
scale_y_continuous(expand = c(0, 0), lim=c(0,40000), label=comma) +
labs(title="CTA Ridership Boxplot By Rail Line") +
theme(plot.title = element_text(hjust=0.5, size=18))

ggplot(subset(within(hist_long, { year <- format(ride_date, '%Y')
year <- as.integer(as.character(year))
}),
year >= 2015 & year <= 2017),
aes(x=factor(year), y=rides_total, fill=rail_line)) +
geom_boxplot() + guides(fill=guide_legend("Rail Lines", nrow=1)) +
scale_fill_manual(values = cta_palette) +
scale_x_discrete(expand = c(0, 0)) +
scale_y_continuous(expand = c(0, 0), lim=c(0,40000), label=comma) +
labs(title="CTA Ridership Boxplot By Rail Line, 2015-2017", x="Year", y="Rides") +
theme(legend.position="bottom", plot.title = element_text(hjust=0.5, size=18))

Correlation
sql <- 'WITH station_agg AS
(SELECT DATE_PART(\'year\', r.ride_date)::integer AS "year",
r.station_id,
r.station_name,
COUNT(r.rides)::numeric(20,5) AS "count",
SUM(r.rides)::numeric(20,5) AS "sum",
AVG(r.rides)::numeric(20,5) AS "mean",
MEDIAN(r.rides)::numeric(20,5) AS "median",
MIN(r.rides)::numeric(20,5) AS "min",
MAX(r.rides)::numeric(20,5) AS "max"
FROM rail_rides r
GROUP BY DATE_PART(\'year\', r.ride_date),
r.station_id,
r.station_name
),
merge_rail AS
(SELECT s.*,
r.rail_line,
(s."sum" / COUNT(*) OVER(PARTITION BY s.station_id, "year")) AS rail_total
FROM station_agg s
INNER JOIN rail_stations r ON s.station_id = r.station_id
)
SELECT m."year",
SUM(rail_total) FILTER (WHERE rail_line = \'blue\') AS blue,
SUM(rail_total) FILTER (WHERE rail_line = \'brown\') AS brown,
SUM(rail_total) FILTER (WHERE rail_line = \'green\') AS green,
SUM(rail_total) FILTER (WHERE rail_line = \'orange\') AS orange,
SUM(rail_total) FILTER (WHERE rail_line = \'pink\') AS pink,
SUM(rail_total) FILTER (WHERE rail_line = \'purple\') AS purple,
SUM(rail_total) FILTER (WHERE rail_line = \'purple_exp\') AS purple_exp,
SUM(rail_total) FILTER (WHERE rail_line = \'red\') AS red,
SUM(rail_total) FILTER (WHERE rail_line = \'yellow\') AS yellow
FROM merge_rail m
GROUP BY m."year"
ORDER BY m."year"'
wide_sql <- dbGetQuery(conn, sql)
wide_sql
cor(wide_sql[-1], use = "complete.obs", method="pearson")
blue brown green orange pink purple
blue 1.0000000 0.9735257 0.8909543 0.9490871 0.8764534 0.7780920
brown 0.9735257 1.0000000 0.8839102 0.9165204 0.8888845 0.7446475
green 0.8909543 0.8839102 1.0000000 0.9295203 0.8768352 0.7684971
orange 0.9490871 0.9165204 0.9295203 1.0000000 0.8697865 0.8742380
pink 0.8764534 0.8888845 0.8768352 0.8697865 1.0000000 0.5573137
purple 0.7780920 0.7446475 0.7684971 0.8742380 0.5573137 1.0000000
purple_exp 0.9707673 0.9785738 0.9264667 0.9722522 0.8994040 0.8304641
red 0.8931410 0.8892051 0.7947539 0.9334301 0.7611228 0.9142396
yellow 0.7415022 0.7744762 0.7812455 0.7698821 0.6214899 0.7375434
purple_exp red yellow
blue 0.9707673 0.8931410 0.7415022
brown 0.9785738 0.8892051 0.7744762
green 0.9264667 0.7947539 0.7812455
orange 0.9722522 0.9334301 0.7698821
pink 0.8994040 0.7611228 0.6214899
purple 0.8304641 0.9142396 0.7375434
purple_exp 1.0000000 0.9355244 0.7854706
red 0.9355244 1.0000000 0.7241108
yellow 0.7854706 0.7241108 1.0000000
T-tests
combns <- as.list(outer(rail_lines, rail_lines, function(x, y) ifelse(x==y, NA, paste(x, y))))
combns <- lapply(combns[!is.na(combns)], function(x) strsplit(x, split=" ")[[1]])
ttest_matrix <- sapply(combns, function(x){
t <- t.test(wide_sql[[x[1]]], wide_sql[[x[2]]])
c(statistic = t$statistic, p_value = t$p.value)
})
ttest_df <- data.frame(x = sapply(combns, "[", 1),
y = sapply(combns, "[", 2),
statistic = ttest_matrix[1,],
p_value = ttest_matrix[2,])
ttest_df <- with(ttest_df, ttest_df[order(x, y),])
ttest_df
by(ttest_df, ttest_df$x, function(sub)
ggplot(sub, aes(x, statistic, fill=y)) + geom_col(position = "dodge") +
labs(title=paste0("CTA System 'L' Lines - Pairwise T-tests for ",
toupper(substr(sub$x[[1]], 1, 1)),
substr(sub$x[[1]], 2, nchar(as.character(sub$x[[1]]))), " Line"),
x="Rail Line", y="T-test Stat") +
scale_x_discrete(expand = c(0,0)) +
scale_y_continuous(expand = c(0, 0), label=comma) +
scale_fill_manual(values = cta_palette[!names(cta_palette)==sub$x[[1]]]) +
guides(fill=guide_legend("Rail Lines", nrow=1)) +
theme(legend.position="bottom",
plot.title = element_text(hjust=0.5, size=18, colour=cta_palette[names(cta_palette)==sub$x[[1]]]),
axis.text.x = element_text(angle=0, hjust=0.5))
)
ttest_df$x: blue

--------------------------------------------------------
ttest_df$x: brown

--------------------------------------------------------
ttest_df$x: green

--------------------------------------------------------
ttest_df$x: orange

--------------------------------------------------------
ttest_df$x: pink

--------------------------------------------------------
ttest_df$x: purple

--------------------------------------------------------
ttest_df$x: purple_exp

--------------------------------------------------------
ttest_df$x: red

--------------------------------------------------------
ttest_df$x: yellow

CSV Regression
Right-Hand Side Variables
Again, requires maintenance and storage on disk space
Again, requires load time from non-centralized paths
Again, requires any ad-hoc munging for usability
# Source: St. Louis Federal Reserve Bank
unemployment_df <- read.csv('Chicago_Unemployment_Rates.csv')
unemployment_df$Date <- as.Date(unemployment_df$Date, format='%Y-%m-%d', origin='1970-01-01')
head(unemployment_df)
# Source: U.S. Department of Energy: EIA
gas_prices_df <- read.csv('US_Gas_Prices.csv')
gas_prices_df$Date <- as.Date(gas_prices_df$Date, format='%Y-%m-%d', origin='1970-01-01')
head(gas_prices_df)
# Source: U.S. National Oceanic and Atmospheric Administration (NOAA)
weather_df <- read.csv('Chicago_Weather_Data.csv')
weather_df$Date <- as.Date(weather_df$Date, format='%Y-%m-%d', origin='1970-01-01')
head(weather_df)
Bus Model Data
Echoes set-based joins
Repetitive sourcing of same object
Nested dense processing of steps
bus_model_data <- merge(unique(bus_month_csv[c("route", "routename")]), bus_rides_csv, by="route")
bus_model_data <- merge(bus_model_data, unemployment_df, by.x='date', by.y='Date')
bus_model_data <- merge(bus_model_data, gas_prices_df, by.x='date', by.y='Date')
bus_model_data <- merge(bus_model_data, weather_df, by.x='date', by.y='Date')
head(bus_model_data)
Add Seasons
bus_model_data$normalized_dt <- as.POSIXlt(bus_model_data$date)
bus_model_data$normalized_dt$year <- bus_model_data$normalized_dt$year +
(2099 - as.integer(format(bus_model_data$date, "%Y")))
bus_model_data$normalized_dt <- as.Date(bus_model_data$normalized_dt)
bus_model_data$season <- ifelse(bus_model_data$normalized_dt >= '2099-01-01' &
bus_model_data$normalized_dt < '2099-03-19', 'winter',
ifelse(bus_model_data$normalized_dt >= '2099-03-20' &
bus_model_data$normalized_dt < '2099-06-19', 'spring',
ifelse(bus_model_data$normalized_dt >= '2099-06-20' &
bus_model_data$normalized_dt < '2099-09-19', 'summer',
ifelse(bus_model_data$normalized_dt >= '2099-09-20' &
bus_model_data$normalized_dt < '2099-12-19', 'fall',
ifelse(bus_model_data$normalized_dt >= '2099-12-20' &
bus_model_data$normalized_dt < '2099-12-31', 'winter',
NA)
)
)
)
)
bus_model_data[sample(nrow(bus_model_data), 10), c("normalized_dt", "date", "season")]
bus_model_data$normalized_dt <- NULL
Bus Modeling (Ordinary Least Squares)
model <- lm(rides ~ daytype + season + UE_Rate + Gas_Price + AvgTemp + Precipitation + SnowDepth,
data = bus_model_data)
summary(model)
Call:
lm(formula = rides ~ daytype + season + UE_Rate + Gas_Price +
AvgTemp + Precipitation + SnowDepth, data = bus_model_data)
Residuals:
Min 1Q Median 3Q Max
-7661 -4936 -2141 3321 38050
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 5209.6046 51.7800 100.610 < 0.0000000000000002 ***
daytypeU -1499.4540 32.9522 -45.504 < 0.0000000000000002 ***
daytypeW 681.4509 25.0499 27.204 < 0.0000000000000002 ***
seasonspring -255.0354 22.8648 -11.154 < 0.0000000000000002 ***
seasonsummer -644.0198 28.4167 -22.663 < 0.0000000000000002 ***
seasonwinter -373.8617 26.7128 -13.996 < 0.0000000000000002 ***
UE_Rate 99.9726 4.1670 23.991 < 0.0000000000000002 ***
Gas_Price -27.0833 10.4389 -2.594 0.00947 **
AvgTemp 11.9167 0.7148 16.671 < 0.0000000000000002 ***
Precipitation -339.3977 23.2796 -14.579 < 0.0000000000000002 ***
SnowDepth -9.7763 5.1624 -1.894 0.05826 .
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 6401 on 665624 degrees of freedom
(148560 observations deleted due to missingness)
Multiple R-squared: 0.01444, Adjusted R-squared: 0.01443
F-statistic: 975.5 on 10 and 665624 DF, p-value: < 0.00000000000000022
Rail Model Data
Assign Latitude and Longitude
rail_long$latitude <- as.numeric(gsub("\\(", "", gsub(",", "", sapply(as.character(rail_long$location),
function(x) strsplit(x, split="\\s+")[[1]][1])))
)
rail_long$longitude <- as.numeric(gsub("\\)", "", sapply(as.character(rail_long$location),
function(x) strsplit(x, split="\\s+")[[1]][2]))
)
rail_long[sample(nrow(rail_long), 10), c("location", "latitude", "longitude")]
rail_model_data <- merge(rail_long, rail_rides_csv, by="station_id")
rail_model_data$raw <- rail_model_data$rides
rail_model_data$rides <- with(rail_model_data, rides /
ave(station_id, station_id, date, FUN=length))
rail_model_data <- merge(rail_model_data, unemployment_df, by.x='date', by.y='Date')
rail_model_data <- merge(rail_model_data, gas_prices_df, by.x='date', by.y='Date')
rail_model_data <- merge(rail_model_data, weather_df, by.x='date', by.y='Date')
head(rail_model_data, 10)
Add Seasons
rail_model_data$normalized_dt <- as.POSIXlt(rail_model_data$date)
rail_model_data$normalized_dt$year <- rail_model_data$normalized_dt$year +
(2099 - as.integer(format(rail_model_data$date, "%Y")))
rail_model_data$normalized_dt <- as.Date(rail_model_data$normalized_dt)
rail_model_data$season <- ifelse(rail_model_data$normalized_dt >= '2099-01-01' &
rail_model_data$normalized_dt < '2099-03-19', 'winter',
ifelse(rail_model_data$normalized_dt >= '2099-03-20' &
rail_model_data$normalized_dt < '2099-06-19', 'spring',
ifelse(rail_model_data$normalized_dt >= '2099-06-20' &
rail_model_data$normalized_dt < '2099-09-19', 'summer',
ifelse(rail_model_data$normalized_dt >= '2099-09-20' &
rail_model_data$normalized_dt < '2099-12-19', 'fall',
ifelse(rail_model_data$normalized_dt >= '2099-12-20' &
rail_model_data$normalized_dt < '2099-12-31', 'winter',
NA)
)
)
)
)
rail_model_data[sample(nrow(rail_model_data), 10), c("normalized_dt", "date", "season")]
rail_model_data$normalized_dt <- NULL
Rail Modeling (Ordinary Least Squares)
model <- lm(rides ~ daytype + season + latitude + longitude + rail_line +
UE_Rate + Gas_Price + AvgTemp + Precipitation + SnowDepth,
data = rail_model_data)
summary(model)
Call:
lm(formula = rides ~ daytype + season + latitude + longitude +
rail_line + UE_Rate + Gas_Price + AvgTemp + Precipitation +
SnowDepth, data = rail_model_data)
Residuals:
Min 1Q Median 3Q Max
-6252.1 -933.1 -256.4 537.0 30569.2
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -67697.5885 3581.3622 -18.90 < 0.0000000000000002
daytypeU -453.5025 6.6974 -67.71 < 0.0000000000000002
daytypeW 1160.6410 5.3328 217.64 < 0.0000000000000002
seasonspring -178.8823 5.3762 -33.27 < 0.0000000000000002
seasonsummer -189.5721 6.6870 -28.35 < 0.0000000000000002
seasonwinter -157.0931 6.2731 -25.04 < 0.0000000000000002
latitude -3995.6159 36.9560 -108.12 < 0.0000000000000002
longitude -2703.3418 44.1307 -61.26 < 0.0000000000000002
rail_linebrown -1180.7391 7.2879 -162.01 < 0.0000000000000002
rail_linegreen -2070.2382 6.9165 -299.32 < 0.0000000000000002
rail_lineorange -1077.6339 8.3798 -128.60 < 0.0000000000000002
rail_linepink -2211.9982 7.4465 -297.05 < 0.0000000000000002
rail_linepurple -2027.7884 11.2705 -179.92 < 0.0000000000000002
rail_linepurple_exp -1547.7298 7.7477 -199.77 < 0.0000000000000002
rail_linered 1640.2397 6.9847 234.83 < 0.0000000000000002
rail_lineyellow -1400.7779 17.7956 -78.72 < 0.0000000000000002
UE_Rate -5.1186 0.9920 -5.16 0.000000247
Gas_Price 185.7348 2.4565 75.61 < 0.0000000000000002
AvgTemp 5.0953 0.1679 30.34 < 0.0000000000000002
Precipitation -82.5539 5.3670 -15.38 < 0.0000000000000002
SnowDepth 1.4842 1.2067 1.23 0.219
(Intercept) ***
daytypeU ***
daytypeW ***
seasonspring ***
seasonsummer ***
seasonwinter ***
latitude ***
longitude ***
rail_linebrown ***
rail_linegreen ***
rail_lineorange ***
rail_linepink ***
rail_linepurple ***
rail_linepurple_exp ***
rail_linered ***
rail_lineyellow ***
UE_Rate ***
Gas_Price ***
AvgTemp ***
Precipitation ***
SnowDepth
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 1850 on 1006966 degrees of freedom
(223613 observations deleted due to missingness)
Multiple R-squared: 0.4033, Adjusted R-squared: 0.4033
F-statistic: 3.403e+04 on 20 and 1006966 DF, p-value: < 0.00000000000000022
SQL Regression
Advanced preparation of data
Materialized view facilitates reproducible research
Compact and straightforward data sourcing
Bus Modeling Data
CREATE MATERIALIZED VIEW Bus_Model_Data AS
SELECT b.id, b.route_id, b.ride_date, b.day_type, b.rides, r.route_name,
CASE
WHEN b.normalized_date BETWEEN '2099-01-01' AND '2099-03-19' THEN 'winter'
WHEN b.normalized_date BETWEEN '2099-03-20' AND '2099-06-19' THEN 'spring'
WHEN b.normalized_date BETWEEN '2099-06-20' AND '2099-09-19' THEN 'summer'
WHEN b.normalized_date BETWEEN '2099-09-20' AND '2099-12-19' THEN 'fall'
WHEN b.normalized_date BETWEEN '2099-12-20' AND '2099-12-31' THEN 'winter'
ELSE NULL
END As season,
ue.ue_rate, g.gas_price, w.avg_temp, w.precipitation, w.snow_depth
FROM
(
SELECT id, route_id, day_type, rides, ride_date,
ride_date + (2099 - date_part('year', ride_date) ||' year')::interval as normalized_date
FROM bus_rides
) b
INNER JOIN bus_routes r ON b.route_id = r.route_id
INNER JOIN unemployment_rates ue ON ue.ue_date = b.ride_date
INNER JOIN gas_prices g ON g.gas_date = b.ride_date
INNER JOIN weather_data w ON w.weather_date = b.ride_date
ORDER BY b.ride_date, NULLIF(regexp_replace(b.route_id, '\D', '', 'g'), '')::int;
REFRESH MATERIALIZED VIEW Bus_Model_Data;
bus_model_data <- dbGetQuery(conn, "SELECT * FROM bus_model_data")
head(bus_model_data)
Bus Modeling (Ordinary Least Squares)
model <- lm(rides ~ day_type + season + ue_rate + gas_price + avg_temp + precipitation + snow_depth,
data = bus_model_data)
summary(model)
Call:
lm(formula = rides ~ day_type + season + ue_rate + gas_price +
avg_temp + precipitation + snow_depth, data = bus_model_data)
Residuals:
Min 1Q Median 3Q Max
-7813 -5072 -2142 3487 37904
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 5290.1066 52.9123 99.979 <0.0000000000000002 ***
day_typeU -1512.5779 33.7077 -44.873 <0.0000000000000002 ***
day_typeW 719.1988 25.6034 28.090 <0.0000000000000002 ***
seasonspring -259.2512 23.5676 -11.000 <0.0000000000000002 ***
seasonsummer -644.0360 29.1817 -22.070 <0.0000000000000002 ***
seasonwinter -384.3928 27.1703 -14.148 <0.0000000000000002 ***
ue_rate 99.6607 4.2720 23.329 <0.0000000000000002 ***
gas_price -19.6097 10.7135 -1.830 0.0672 .
avg_temp 12.1726 0.7301 16.673 <0.0000000000000002 ***
precipitation -350.3917 23.6077 -14.842 <0.0000000000000002 ***
snow_depth -9.6659 5.3147 -1.819 0.0690 .
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 6483 on 648166 degrees of freedom
(133403 observations deleted due to missingness)
Multiple R-squared: 0.01478, Adjusted R-squared: 0.01477
F-statistic: 972.7 on 10 and 648166 DF, p-value: < 0.00000000000000022
Rail Modeling Data
CREATE MATERIALIZED VIEW Rail_Model_Data AS
SELECT r.id, r.station_id, r.station_name, r.ride_date, r.day_type, r.rides AS raw,
(r.rides / COUNT(*) OVER(PARTITION BY r.station_id, r.ride_date)) AS rides,
CASE
WHEN r.normalized_date BETWEEN '2099-01-01' AND '2099-03-19' THEN 'winter'
WHEN r.normalized_date BETWEEN '2099-03-20' AND '2099-06-19' THEN 'spring'
WHEN r.normalized_date BETWEEN '2099-06-20' AND '2099-09-19' THEN 'summer'
WHEN r.normalized_date BETWEEN '2099-09-20' AND '2099-12-19' THEN 'fall'
WHEN r.normalized_date BETWEEN '2099-12-20' AND '2099-12-31' THEN 'winter'
ELSE NULL
END As season,
REPLACE(REPLACE((regexp_split_to_array(s.location, '\s+'))[1], ',', ''), '(', '')::numeric AS latitude,
REPLACE((regexp_split_to_array(s.location, '\s+'))[2], ')', '')::numeric AS longitude,
s.rail_line, s.ada, s.direction,
ue.ue_rate, g.gas_price, w.avg_temp, w.precipitation, w.snow_depth
FROM
(
SELECT id, station_id, station_name, day_type, rides, ride_date,
ride_date + (2099 - date_part('year', ride_date) ||' year')::interval as normalized_date
FROM rail_rides
)r
INNER JOIN rail_stations s ON s.station_id = r.station_id
INNER JOIN unemployment_rates ue ON ue.ue_date = r.ride_date
INNER JOIN gas_prices g ON g.gas_date = r.ride_date
INNER JOIN weather_data w ON w.weather_date = r.ride_date
ORDER BY r.ride_date, r.station_id;
REFRESH MATERIALIZED VIEW Rail_Model_Data;
rail_model_data <- dbGetQuery(conn, "SELECT * FROM rail_model_data")
head(rail_model_data)
Rail Modeling (Ordinary Least Squares)
model <- lm(rides ~ day_type + season + latitude + longitude + rail_line +
ue_rate + gas_price + avg_temp + precipitation + snow_depth,
data = rail_model_data)
summary(model)
Call:
lm(formula = rides ~ day_type + season + latitude + longitude +
rail_line + ue_rate + gas_price + avg_temp + precipitation +
snow_depth, data = rail_model_data)
Residuals:
Min 1Q Median 3Q Max
-6249.5 -932.9 -256.2 536.9 30570.4
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -68149.8607 3556.3642 -19.163 < 0.0000000000000002
day_typeU -454.4689 6.6549 -68.290 < 0.0000000000000002
day_typeW 1157.9025 5.2941 218.717 < 0.0000000000000002
seasonspring -177.8150 5.3603 -33.172 < 0.0000000000000002
seasonsummer -185.4726 6.6439 -27.916 < 0.0000000000000002
seasonwinter -160.7995 6.1740 -26.044 < 0.0000000000000002
latitude -3995.1405 36.6987 -108.863 < 0.0000000000000002
longitude -2708.3027 43.8227 -61.801 < 0.0000000000000002
rail_linebrown -1180.4292 7.2371 -163.109 < 0.0000000000000002
rail_linegreen -2069.6169 6.8682 -301.334 < 0.0000000000000002
rail_lineorange -1077.4023 8.3213 -129.476 < 0.0000000000000002
rail_linepink -2211.5330 7.3945 -299.076 < 0.0000000000000002
rail_linepurple -2027.3643 11.1919 -181.146 < 0.0000000000000002
rail_linepurple_exp -1547.6652 7.6936 -201.163 < 0.0000000000000002
rail_linered 1641.3142 6.9360 236.637 < 0.0000000000000002
rail_lineyellow -1400.4416 17.6707 -79.252 < 0.0000000000000002
ue_rate -5.3018 0.9843 -5.387 0.0000000718
gas_price 185.5411 2.4399 76.046 < 0.0000000000000002
avg_temp 5.0833 0.1659 30.635 < 0.0000000000000002
precipitation -83.1416 5.2864 -15.727 < 0.0000000000000002
snow_depth 1.6746 1.2015 1.394 0.163
(Intercept) ***
day_typeU ***
day_typeW ***
seasonspring ***
seasonsummer ***
seasonwinter ***
latitude ***
longitude ***
rail_linebrown ***
rail_linegreen ***
rail_lineorange ***
rail_linepink ***
rail_linepurple ***
rail_linepurple_exp ***
rail_linered ***
rail_lineyellow ***
ue_rate ***
gas_price ***
avg_temp ***
precipitation ***
snow_depth
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 1850 on 1021214 degrees of freedom
(209365 observations deleted due to missingness)
Multiple R-squared: 0.4031, Adjusted R-squared: 0.4031
F-statistic: 3.449e+04 on 20 and 1021214 DF, p-value: < 0.00000000000000022
# DISCONNECT FROM DATABASE
dbDisconnect(conn)
[1] TRUE
Conclusions
Postgres provides a stable, centralized, repository for data sourcing
Postgres maintains an expressive SQL dialect for data processing
Postgres supports data science with vectorized methods and reproducibility
LS0tCnRpdGxlOiAiTGV2ZXJhZ2luZyBQb3N0Z3JlU1FMIGluIERhdGEgU2NpZW5jZSB3aXRoIFIiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCjxkaXYgc3R5bGU9ImZsb2F0OnJpZ2h0Ij48aW1nIHNyYz0icG9zdGdyZXNxbF9yLnBuZyIgaGVpZ2h0PSIyMDAiIHdpZHRoPSIyMDAiLz48L2Rpdj4KCjxkaXYgc3R5bGU9ImZvbnQtc2l6ZTogMjRweDsiPkhvdyBQR1NRTCBjYW4gZmFjaWxpdGF0ZSB0aGUgZGF0YSBwcm9jZXNzaW5nIHdvcmtmbG93PC9kaXY+CiMjIFBhcmZhaXQgR2FzYW5hICMjCjxkaXYgc3R5bGU9ImZvbnQtc2l6ZTogMjBweDsiPkRhdGEgQW5hbHlzdCwgV2luc3RvbiAmIFN0cmF3bjwvZGl2PgoKPHN0eWxlPgpkaXYuYmx1ZSBwcmUgeyBiYWNrZ3JvdW5kLWNvbG9yOiAjRUJGNEZBOyB9Cjwvc3R5bGU+CgpgYGB7cn0KIyMgTGlicmFyaWVzCmxpYnJhcnkoREJJKQpsaWJyYXJ5KFJQb3N0Z3JlU1FMKQpsaWJyYXJ5KG1pY3JvYmVuY2htYXJrKQoKbGlicmFyeShzY2FsZXMpCmxpYnJhcnkoZ2dwbG90MikKCm9wdGlvbnMocmVwci5wbG90LndpZHRoPTE1LCByZXByLnBsb3QuaGVpZ2h0PTUpCm9wdGlvbnMoc2NpcGVuPTk5OSkKCmBgYAoKIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNjQ2NDY0Ij5JbXBvcnQgQ1NWIERhdGE8L3NwYW4+CgotICMjIyA8c3BhbiBzdHlsZT0iY29sb3I6ICM2NDY0NjQiPk9mdGVuIG9uZSBvZiB0aGUgbGFyZ2VzdCB0aW1lIGFuZCByZXNvdXJjZS1pbnRlbnNpdmUgc3RlcHMgZm9yIGRhdGEgYW5hbHlzdHM8L3NwYW4+ICMjIwotICMjIyA8c3BhbiBzdHlsZT0iY29sb3I6ICM2NDY0NjQiPlJlcXVpcmVzIGRpc2sgc3BhY2Ugb24gc2hhcmVkIG5ldHdvcmsgb3IgbG9jYWwgZHJpdmVzPC9zcGFuPiAjIyMKLSAjIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNjQ2NDY0Ij5NdWx0aXBsZSBmaWxlcyB1c3VhbGx5IHVubWFuYWdlZCBhY3Jvc3MgdmVyc2lvbnMgYW5kIHRlYW1zPC9zcGFuPiAjIyMKCmBgYHtyfQpjc3ZfaW1wb3J0IDwtIGZ1bmN0aW9uKCkgewogICAgc2V0d2QoIn4vRG9jdW1lbnRzL1BHU1FMIikKICAgIAogICAgYnVzX21vbnRoIDwtIHJlYWQuY3N2KCdDVEFfLV9SaWRlcnNoaXBfLV9CdXNfUm91dGVzXy1fTW9udGhseV9EYXktVHlwZV9BdmVyYWdlc19fX1RvdGFscy5jc3YnKQogICAgYnVzX3JpZGVzIDwtIHJlYWQuY3N2KCdDVEFfLV9SaWRlcnNoaXBfLV9CdXNfUm91dGVzXy1fRGFpbHlfVG90YWxzX2J5X1JvdXRlLmNzdicpCgogICAgcmFpbF9zdG9wcyA8LSByZWFkLmNzdignQ1RBXy1fU3lzdGVtX0luZm9ybWF0aW9uXy1fTGlzdF9vZl9fTF9fU3RvcHMuY3N2JykKICAgIHJhaWxfcmlkZXMgPC0gcmVhZC5jc3YoJ0NUQV8tX1JpZGVyc2hpcF8tX19MX19TdGF0aW9uX0VudHJpZXNfLV9EYWlseV9Ub3RhbHMuY3N2JykKfQpgYGAKCiMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5JbXBvcnQgU1FMIERhdGE8L3NwYW4+CgotICMjIyA8c3BhbiBzdHlsZT0iY29sb3I6IzMzNjc5MSI+Q2VudHJhbGl6ZWQgZGF0YSBhY2Nlc3MgbG9jYWxseSBvciByZW1vdGVseTwvc3Bhbj4gIyMjCiAgICAtICMjIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPlJlbGF0aW9uYWwgbW9kZWwgZW5zdXJlcyBubyByZWR1bmRhbmN5PC9zcGFuPiAjIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5TZWN1cmVkIGFuZCByb2J1c3QgZGF0YSBzdG9yYWdlPC9zcGFuPiAjIyMKICAgIC0gIyMjIyA8c3BhbiBzdHlsZT0iY29sb3I6IzMzNjc5MSI+QXZhaWxhYmxlIGJhY2t1cCB3aXRoIG5vIG5lZWQgb2YgbG9jYWwgY29tcHV0ZXJzPC9zcGFuPiAjIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5UYWJsZSBhbmQgc2V0IHJlbGF0aW9ucyBzZWFtbGVzcyBpbnRlZ3JhdGVzIGludG8gZGF0YXNldHM8L3NwYW4+ICMjIwogICAgLSAjIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5hdG9taWMgY29sdW1ucyBhbmQgZGl2ZXJzZSByb3dzZXQ8L3NwYW4+ICMjIyMKCjxkaXYgY2xhc3M9ImJsdWUiPgpgYGB7cn0Kc3FsX2ltcG9ydCA8LSBmdW5jdGlvbigpIHsKICAgIGNvbm4gPC0gZGJDb25uZWN0KFJQb3N0Z3JlU1FMOjpQb3N0Z3JlU1FMKCksIGhvc3Q9IjEwLjAuMC4yMjAiLCBkYm5hbWU9ImN0YSIsCiAgICAgICAgICAgICAgICAgICAgICB1c2VyPSJjdGFkYmEiLCBwYXNzd29yZD0iY3RhMTghIiwgcG9ydD01NDMyKQoKICAgIGJ1c19tb250aCA8LSBkYkdldFF1ZXJ5KGNvbm4sICJTRUxFQ1QgKiBGUk9NIGJ1c19tb250aCIpCiAgICBidXNfcmlkZXMgPC0gZGJHZXRRdWVyeShjb25uLCAiU0VMRUNUICogRlJPTSBidXNfcmlkZXMiKQoKICAgIHJhaWxfc3RvcHMgPC0gZGJHZXRRdWVyeShjb25uLCAiU0VMRUNUICogRlJPTSByYWlsX3N0b3BzIikKICAgIHJhaWxfcmlkZXMgPC0gZGJHZXRRdWVyeShjb25uLCAiU0VMRUNUICogRlJPTSByYWlsX3JpZGVzIikKCiAgICBkYkRpc2Nvbm5lY3QoY29ubikKfQoKc3VtbWFyeShtaWNyb2JlbmNobWFyayhjc3ZfaW1wb3J0KSkKc3VtbWFyeShtaWNyb2JlbmNobWFyayhzcWxfaW1wb3J0KSkKCmBgYAo8L2Rpdj4KCiMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+VmlldyBDU1YgRGF0YTwvc3Bhbj4KCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+T2Z0ZW4gcmVxdWlyZXMgYWQtaG9jIG11bmdpbmc6IHJlLWZvcm1hdHRpbmcgb3IgY29udmVyc2lvbiBvZiBjb2x1bW5zPC9zcGFuPiAjIyMKLSAjIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNjQ2NDY0Ij5DbGVhbmluZyBvdXQgcmVwb3J0cyBhbmQgbWV0YWRhdGEgZnJvbSBhY3R1YWwgZGF0YSBvbiB0aGUgZmx5PC9zcGFuPiAjIyMKLSAjIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNjQ2NDY0Ij5BZGp1c3RpbmcgZGlmZmVyZW50IGxldmVscyBhbmQgcmVzaGFwaW5nIGZvciBhbmFseXRpY2FsIHB1cnBvc2VzPC9zcGFuPiAjIyMKCmBgYHtyfQpidXNfbW9udGhfY3N2IDwtIHJlYWQuY3N2KCdDVEFfLV9SaWRlcnNoaXBfLV9CdXNfUm91dGVzXy1fTW9udGhseV9EYXktVHlwZV9BdmVyYWdlc19fX1RvdGFscy5jc3YnKQpoZWFkKGJ1c19tb250aF9jc3YpCgpidXNfcmlkZXNfY3N2IDwtIHJlYWQuY3N2KCdDVEFfLV9SaWRlcnNoaXBfLV9CdXNfUm91dGVzXy1fRGFpbHlfVG90YWxzX2J5X1JvdXRlLmNzdicpCmhlYWQoYnVzX3JpZGVzX2NzdikKCnJhaWxfc3RvcHNfY3N2IDwtIHJlYWQuY3N2KCdDVEFfLV9TeXN0ZW1fSW5mb3JtYXRpb25fLV9MaXN0X29mX19MX19TdG9wcy5jc3YnKQpuYW1lcyhyYWlsX3N0b3BzX2NzdikgPC0gdG9sb3dlcihuYW1lcyhyYWlsX3N0b3BzX2NzdikpCmhlYWQocmFpbF9zdG9wc19jc3YpCgpyYWlsX3JpZGVzX2NzdiA8LSByZWFkLmNzdignQ1RBXy1fUmlkZXJzaGlwXy1fX0xfX1N0YXRpb25fRW50cmllc18tX0RhaWx5X1RvdGFscy5jc3YnKQpoZWFkKHJhaWxfcmlkZXNfY3N2KQpgYGAKCmBgYHtyfQpidXNfbW9udGhfY3N2JE1vbnRoX0JlZ2lubmluZyA8LSBhcy5EYXRlKGJ1c19tb250aF9jc3YkTW9udGhfQmVnaW5uaW5nLCBmb3JtYXQ9IiVtLyVkLyVZIiwgb3JpZ2luPSIxOTcwLTAxLTAxIikKCmJ1c19yaWRlc19jc3YkZGF0ZSA8LSBhcy5EYXRlKGJ1c19yaWRlc19jc3YkZGF0ZSwgZm9ybWF0PSIlbS8lZC8lWSIsIG9yaWdpbj0iMTk3MC0wMS0wMSIpCgpyYWlsX3JpZGVzX2NzdiRkYXRlIDwtIGFzLkRhdGUocmFpbF9yaWRlc19jc3YkZGF0ZSwgZm9ybWF0PSIlbS8lZC8lWSIsIG9yaWdpbj0iMTk3MC0wMS0wMSIpCmBgYAoKIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPlZpZXcgU1FMIERhdGE8L3NwYW4+CgotICMjIyA8c3BhbiBzdHlsZT0iY29sb3I6IzMzNjc5MSI+UmVsYXRpb25hbCBkYXRhYmFzZXMgYXJlIHBsYW5uZWQgc3lzdGVtczwvc3Bhbj4gIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5Db25zdHJhaW50cyBhbmQgcmVmZXJlbnRpYWwgaW50ZWdyaXR5IGVuc3VyZXMgc3RhYmlsaXR5IGFuZCByZWxhdGlvbnNoaXBzPC9zcGFuPiAjIyMKLSAjIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPlF1ZXJ5IGxhbmd1YWdlIHByb3ZpZGVzIGV4cHJlc3Npb24sIHJlbGlhYmxpYmlsaXR5IGFuZCB2ZXJzYXRpbGl0eTwvc3Bhbj4gIyMjCgo8ZGl2IGNsYXNzPSJibHVlIj4KYGBge3J9CmNvbm4gPC0gZGJDb25uZWN0KFJQb3N0Z3JlU1FMOjpQb3N0Z3JlU1FMKCksIGhvc3Q9IjEwLjAuMC4yMjAiLCBkYm5hbWU9ImN0YSIsCiAgICAgICAgICAgICAgICAgIHVzZXI9ImN0YWRiYSIsIHBhc3N3b3JkPSJjdGExOCEiLCBwb3J0PTU0MzIpCgpidXNfbW9udGhfc3FsIDwtIGRiR2V0UXVlcnkoY29ubiwgIlNFTEVDVCAqIEZST00gYnVzX21vbnRoIikKaGVhZChidXNfbW9udGhfc3FsKQoKYnVzX3JpZGVzX3NxbCA8LSBkYkdldFF1ZXJ5KGNvbm4sICJTRUxFQ1QgKiBGUk9NIGJ1c19yaWRlcyIpCmhlYWQoYnVzX3JpZGVzX3NxbCkKCnJhaWxfc3RhdGlvbnNfc3FsIDwtIGRiR2V0UXVlcnkoY29ubiwgIlNFTEVDVCAqIEZST00gcmFpbF9zdGF0aW9ucyIpCmhlYWQocmFpbF9zdGF0aW9uc19zcWwpCgpyYWlsX3JpZGVzX3NxbCA8LSBkYkdldFF1ZXJ5KGNvbm4sICJTRUxFQ1QgKiBGUk9NIHJhaWxfcmlkZXMiKQpoZWFkKHJhaWxfcmlkZXNfc3FsKQoKYGBgCjwvZGl2PgoKCiMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+QWdncmVnYXRlIENTViBEYXRhPC9zcGFuPiAKCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+RGF0YSB0b29sIHN5bnRheCBjYW4gYmUgZm9ybWlkYWJsZSBmb3IgbmV3Y29tZXJzIG9yIHJldHVybmluZyB1c2Vyczwvc3Bhbj4gIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+Q29tcGxleCBwcm9jZXNzZXMgcmVxdWlyZSBsb25nIHBpcGluZy9jaGFpbmluZyBvZiBvYmplY3RzIGFuZCBtZXRob2RzPC9zcGFuPiAjIyMKLSAjIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNjQ2NDY0Ij5MYW5ndWFnZSBsYWNrcyBzZXQtYmFzZWQgKGkuZS4sIHJlbGF0aW9ucywgam9pbiwgdW5pb24pIGZyYW1ld29yazwvc3Bhbj4gIyMjCiAgCgpgYGB7cn0KIyBNRVJHRQphZ2dfY3N2IDwtIG1lcmdlKHVuaXF1ZShidXNfbW9udGhfY3N2W2MoInJvdXRlIiwgInJvdXRlbmFtZSIpXSksIGJ1c19yaWRlc19jc3YsIGJ5PSJyb3V0ZSIpCgojIEFHR1JFR0FURQphZ2dfY3N2IDwtIGRvLmNhbGwoZGF0YS5mcmFtZSwgCiAgICAgICAgICAgICAgICAgICBhZ2dyZWdhdGUocmlkZXMgfiByb3V0ZSArIHJvdXRlbmFtZSwgYWdnX2NzdiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZnVuY3Rpb24oeCkgYyhjb3VudD1sZW5ndGgoeCksIHN1bT1zdW0oeCksIG1lYW49bWVhbih4KSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZWRpYW49bWVkaWFuKHgpLCBtaW49bWluKHgpLCBtYXg9bWF4KHgpKSkpCiMgT1JERVIKYWdnX2NzdiA8LSB3aXRoKGFnZ19jc3YsIGFnZ19jc3Zbb3JkZXIoLXJpZGVzLnN1bSksXSkKCmFnZ19jc3YKYGBgCgoKIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+VG9wIDUgQnVzIFJvdXRlczwvc3Bhbj4gCgpgYGB7cn0KIyBNRVJHRQphZ2dfY3N2IDwtIG1lcmdlKHN1YnNldCh1bmlxdWUoYnVzX21vbnRoX2NzdltjKCJyb3V0ZSIsICJyb3V0ZW5hbWUiKV0pLCAKICAgICAgICAgICAgICAgICAgICAgICAgcm91dGVuYW1lICVpbiUgYygiNzl0aCIsICJBc2hsYW5kIiwgIkNoaWNhZ28iLCAiV2VzdGVybiIsICJDb3R0YWdlIEdyb3ZlIikpLAogICAgICAgICAgICAgICAgIHRyYW5zZm9ybShidXNfcmlkZXNfY3N2LCB5ZWFyPWZvcm1hdChkYXRlLCAiJVkiKSksCiAgICAgICAgICAgICAgICAgYnk9InJvdXRlIikKCiMgQUdHUkVHQVRFCmFnZ19jc3YgPC0gZG8uY2FsbChkYXRhLmZyYW1lLCAKICAgICAgICAgICAgICAgICAgIGFnZ3JlZ2F0ZShyaWRlcyB+IHJvdXRlbmFtZSArIHllYXIsIGFnZ19jc3YsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGZ1bmN0aW9uKHgpIGMoY291bnQ9bGVuZ3RoKHgpLCBzdW09c3VtKHgpLCBtZWFuPW1lYW4oeCksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWVkaWFuPW1lZGlhbih4KSwgbWluPW1pbih4KSwgbWF4PW1heCh4KSkpKQojIE9SREVSCmFnZ19jc3YgPC0gd2l0aChhZ2dfY3N2LCBhZ2dfY3N2W29yZGVyKHJvdXRlbmFtZSwgeWVhciksXSkKCmFnZ19jc3YKYGBgCgoKIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNjQ2NDY0Ij5HcmFwaCBDU1YgRGF0YTwvc3Bhbj4gCgpgYGB7ciBmaWcxLCBmaWcuaGVpZ2h0ID0gNCwgZmlnLndpZHRoID0gMTAsIGZpZy5hbGlnbiA9ICJjZW50ZXIifQoKc2VhYm9yblBhbGV0dGUgPC0gYygiIzRjNzJiMCIsIiM1NWE4NjgiLCIjYzQ0ZTUyIiwiIzgxNzJiMiIsIiNjY2I5NzQiLCIjNjRiNWNkIiwiIzRjNzJiMCIsIiM1NWE4NjgiLAogICAgICAgICAgICAgICAgICAgICIjYzQ0ZTUyIiwiIzgxNzJiMiIsIiNjY2I5NzQiLCIjNjRiNWNkIiwiIzRjNzJiMCIsIiM1NWE4NjgiLCIjYzQ0ZTUyIiwiIzgxNzJiMiIsCiAgICAgICAgICAgICAgICAgICAgIiNjY2I5NzQiLCIjNjRiNWNkIiwiIzRjNzJiMCIsIiM1NWE4NjgiLCIjYzQ0ZTUyIiwiIzgxNzJiMiIsIiNjY2I5NzQiLCIjNjRiNWNkIiwKICAgICAgICAgICAgICAgICAgICAiIzRjNzJiMCIsIiM1NWE4NjgiLCIjYzQ0ZTUyIiwiIzgxNzJiMiIsIiNjY2I5NzQiLCIjNjRiNWNkIiwiIzRjNzJiMCIsIiM1NWE4NjgiLAogICAgICAgICAgICAgICAgICAgICIjYzQ0ZTUyIiwiIzgxNzJiMiIsIiNjY2I5NzQiLCIjNjRiNWNkIiwiIzRjNzJiMCIsIiM1NWE4NjgiLCIjYzQ0ZTUyIiwiIzgxNzJiMiIsCiAgICAgICAgICAgICAgICAgICAgIiNjY2I5NzQiLCIjNjRiNWNkIiwiIzRjNzJiMCIsIiM1NWE4NjgiLCIjYzQ0ZTUyIiwiIzgxNzJiMiIsIiNjY2I5NzQiLCIjNjRiNWNkIiwKICAgICAgICAgICAgICAgICAgICAiIzRjNzJiMCIsIiM1NWE4NjgiLCIjYzQ0ZTUyIiwiIzgxNzJiMiIsIiNjY2I5NzQiLCIjNjRiNWNkIiwiIzRjNzJiMCIsIiM1NWE4NjgiLAogICAgICAgICAgICAgICAgICAgICIjYzQ0ZTUyIiwiIzgxNzJiMiIsIiNjY2I5NzQiLCIjNjRiNWNkIikKCmdncGxvdChhZ2dfY3N2LCBhZXMoeWVhciwgcmlkZXMuc3VtLCBmaWxsPXJvdXRlbmFtZSkpICsgZ2VvbV9jb2wocG9zaXRpb24gPSAiZG9kZ2UiKSArCiAgbGFicyh0aXRsZT0iVG9wIDUgQ1RBICdMJyBCdXMgUm91dGVzIiwgeD0iWWVhciIsIHk9IlJpZGVzIikgKwogIHNjYWxlX3lfY29udGludW91cyhleHBhbmQgPSBjKDAsIDApLCBsYWJlbD1jb21tYSkgKwogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IHNlYWJvcm5QYWxldHRlKSArCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uPSJib3R0b20iLAogICAgICAgIHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3Q9MC41LCBzaXplPTE4KSwKICAgICAgICBheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZT0wLCBoanVzdD0wLjUpKQpgYGAKCiMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5BZ2dyZWdhdGUgU1FMIERhdGE8L3NwYW4+IAoKLSAjIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPkNsZWFyLCBjb21wYWN0IGRlY2xhcmF0aXZlIGxhbmd1YWdlIHdpdGggcG9ydGFiaWxpdHk8L3NwYW4+ICMjIyMKLSAjIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPlByb2Nlc3Npbmcgd2l0aCB2aXJ0dWFsIHRhYmxlcyBvY2N1cnMgYmVoaW5kIHRoZSBzY2VuZTwvc3Bhbj4gIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5TZXQtYmFzZWQgZnJhbWV3b3JrIGZhY2lsaXRhdGVzIGJsb2Nrd2lzZSwgdmVjdG9yaXplZCBwcm9jZXNzPC9zcGFuPiAjIyMKCjxkaXYgY2xhc3M9ImJsdWUiPgpgYGB7cn0Kc3FsIDwtICdTRUxFQ1QgcnQucm91dGVfbmFtZSwgQ09VTlQocmQucmlkZXMpIEFTICJjb3VudCIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBTVU0ocmQucmlkZXMpIEFTICJzdW0iLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgQVZHKHJkLnJpZGVzKSBBUyAibWVhbiIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBNRURJQU4ocmQucmlkZXMpIEFTICJtZWRpYW4iLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBNSU4ocmQucmlkZXMpIEFTICJtaW4iLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgTUFYKHJkLnJpZGVzKSBBUyAibWF4IgogICAgICAgIEZST00gYnVzX3JvdXRlcyBydAogICAgICAgIElOTkVSIEpPSU4gYnVzX3JpZGVzIHJkIE9OIHJ0LnJvdXRlX2lkID0gcmQucm91dGVfaWQKICAgICAgICBHUk9VUCBCWSBydC5yb3V0ZV9uYW1lCiAgICAgICAgT1JERVIgQlkgU1VNKHJkLnJpZGVzKSBERVNDJwoKYWdnX3NxbCA8LSBkYkdldFF1ZXJ5KGNvbm4sIHNxbCkKCmFnZ19zcWwKYGBgCgpgYGB7cn0Kc3FsIDwtICdTRUxFQ1QgcnQucm91dGVfbmFtZSwgREFURV9QQVJUKFwneWVhclwnLCByZC5yaWRlX2RhdGUpOjppbnRlZ2VyIEFTICJ5ZWFyIiwgCiAgICAgICAgICAgICBDT1VOVChyZC5yaWRlcykgQVMgImNvdW50IiwgCiAgICAgICAgICAgICBTVU0ocmQucmlkZXMpIEFTICJzdW0iLCAKICAgICAgICAgICAgIEFWRyhyZC5yaWRlcykgQVMgIm1lYW4iLCAKICAgICAgICAgICAgIE1FRElBTihyZC5yaWRlcykgQVMgIm1lZGlhbiIsCiAgICAgICAgICAgICBNSU4ocmQucmlkZXMpIEFTICJtaW4iLCAKICAgICAgICAgICAgIE1BWChyZC5yaWRlcykgQVMgIm1heCIKICAgICAgRlJPTSBidXNfcm91dGVzIHJ0CiAgICAgIElOTkVSIEpPSU4gYnVzX3JpZGVzIHJkIE9OIHJ0LnJvdXRlX2lkID0gcmQucm91dGVfaWQKICAgICAgV0hFUkUgcnQucm91dGVfbmFtZSBJTiAoXCc3OXRoXCcsIFwnQXNobGFuZFwnLCBcJ0NoaWNhZ29cJywgXCdXZXN0ZXJuXCcsIFwnQ290dGFnZSBHcm92ZVwnKQogICAgICBHUk9VUCBCWSBydC5yb3V0ZV9uYW1lLCBEQVRFX1BBUlQoXCd5ZWFyXCcsIHJkLnJpZGVfZGF0ZSk6OmludGVnZXIKICAgICAgT1JERVIgQlkgcnQucm91dGVfbmFtZSwgREFURV9QQVJUKFwneWVhclwnLCByZC5yaWRlX2RhdGUpOjppbnRlZ2VyJwoKYWdnX3NxbCA8LSBkYkdldFF1ZXJ5KGNvbm4sIHNxbCkKCmFnZ19zcWwKYGBgCjwvZGl2PgoKIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPkdyYXBoIFNRTCBEYXRhPC9zcGFuPgoKPGRpdiBjbGFzcz0iYmx1ZSI+CmBgYHtyIGZpZzIsIGZpZy5oZWlnaHQgPSA0LCBmaWcud2lkdGggPSAxMCwgZmlnLmFsaWduID0gImNlbnRlciJ9CmdncGxvdChhZ2dfc3FsLCBhZXMoeWVhciwgc3VtLCBjb2xvcj1yb3V0ZV9uYW1lKSkgKyAKICBnZW9tX2xpbmUoc3RhdD0iaWRlbnRpdHkiKSArIGdlb21fcG9pbnQoc3RhdD0iaWRlbnRpdHkiKSArCiAgbGFicyh0aXRsZT0iVG9wIDUgQ1RBICdMJyBCdXMgUm91dGVzIiwgeD0iWWVhciIsIHk9IlJpZGVzIikgKwogIHNjYWxlX3hfY29udGludW91cygieWVhciIsIGJyZWFrcz11bmlxdWUoYWdnX3NxbCR5ZWFyKSkgKwogIHNjYWxlX3lfY29udGludW91cyhleHBhbmQgPSBjKDAsIDApLCBsYWJlbD1jb21tYSkgKwogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXMgPSBzZWFib3JuUGFsZXR0ZSkgKwogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbj0iYm90dG9tIiwKICAgICAgICBwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0PTAuNSwgc2l6ZT0xOCksCiAgICAgICAgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGU9MCwgaGp1c3Q9MC41KSkKYGBgCjwvZGl2PgoKIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNjQ2NDY0Ij5DU1YgRGF0YSBEaWFnbm9zdGljczwvc3Bhbj4gIyMKCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+SW1wZXJhdGl2ZSBuYXR1cmUgb2YgcHJvY2Vzc2luZzwvc3Bhbj4gIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+RGVuc2UsIG5lc3RlZCBjYWxscyBmb3IgbGF5ZXJlZCBzdGVwczwvc3Bhbj4gIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+TGltaXRlZCB0byBhcHBsaWNhdGlvbiBsYXllcjwvc3Bhbj4gIyMjCgpgYGB7cn0KIyBUUkFOU0ZPUk0KYWdnX2NzdiA8LSB0cmFuc2Zvcm0ocmFpbF9yaWRlc19jc3YsIHllYXI9Zm9ybWF0KGRhdGUsICIlWSIpKQoKIyBBR0dSRUdBVEUKYWdnX2NzdiA8LSBkby5jYWxsKGRhdGEuZnJhbWUsIAogICAgICAgICAgICAgICAgICAgYWdncmVnYXRlKHJpZGVzIH4gc3RhdGlvbl9pZCArIHN0YXRpb25uYW1lICsgeWVhciwgYWdnX2NzdiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZnVuY3Rpb24oeCkgYyhjb3VudD1sZW5ndGgoeCksIHN1bT1zdW0oeCksIG1lYW49bWVhbih4KSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZWRpYW49bWVkaWFuKHgpLCBtaW49bWluKHgpLCBtYXg9bWF4KHgpKSkpCiMgT1JERVIKYWdnX2NzdiA8LSB3aXRoKGFnZ19jc3YsIGFnZ19jc3Zbb3JkZXIoLXJpZGVzLnN1bSksXSkKCmFnZ19jc3YKYGBgCgoKYGBge3J9CiMgUkVTSEFQRQpyYWlsX2xpbmVzIDwtIGMoInJlZCIsICJibHVlIiwgImdyZWVuIiwgImJyb3duIiwgInB1cnBsZSIsICJwdXJwbGVfZXhwIiwgInllbGxvdyIsICJwaW5rIiwgIm9yYW5nZSIpCgpyYWlsX2xvbmcgPC0gcmVzaGFwZShzZXROYW1lcyhyYWlsX3N0b3BzX2NzdltjKCJtYXBfaWQiLCAic3RhdGlvbl9uYW1lIiwgImxvY2F0aW9uIiwgInJlZCIsICJibHVlIiwgImciLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYnJuIiwgInAiLCAicGV4cCIsICJ5IiwgInBuayIsICJvIildLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJzdGF0aW9uX2lkIiwgInN0YXRpb25fbmFtZSIsICJsb2NhdGlvbiIsIHJhaWxfbGluZXMpKSwKICAgICAgICAgICAgICAgICAgICAgdmFyeWluZyA9IHJhaWxfbGluZXMsIHYubmFtZXMgPSAidmFsdWUiLCAKICAgICAgICAgICAgICAgICAgICAgdGltZXZhciA9ICJyYWlsX2xpbmUiLCB0aW1lcyA9IHJhaWxfbGluZXMsCiAgICAgICAgICAgICAgICAgICAgIG5ldy5yb3cubmFtZXMgPSAxOjEwMDAwLCBkaXJlY3Rpb24gPSAibG9uZyIpCgojIFNVQlNFVApyYWlsX2xvbmcgPC0gdW5pcXVlKHN1YnNldChyYWlsX2xvbmcsIHZhbHVlPT0ndHJ1ZScpW2MoInN0YXRpb25faWQiLCAic3RhdGlvbl9uYW1lIiwgImxvY2F0aW9uIiwgInJhaWxfbGluZSIpXSkKCiMgT1JERVIKcmFpbF9sb25nIDwtIHdpdGgocmFpbF9sb25nLCByYWlsX2xvbmdbb3JkZXIoc3RhdGlvbl9pZCwgcmFpbF9saW5lKSxdKQpyb3cubmFtZXMocmFpbF9sb25nKSA8LSBOVUxMCgpyYWlsX2xvbmcKYGBgCgoKYGBge3J9Cm1lcmdlX3JhaWwgPC0gbWVyZ2UoYWdnX2NzdiwgcmFpbF9sb25nLCBieT0ic3RhdGlvbl9pZCIpCgptZXJnZV9yYWlsJHJpZGVzLnRvdGFsIDwtIG1lcmdlX3JhaWwkcmlkZXMuc3VtIC8gd2l0aChtZXJnZV9yYWlsLCBhdmUoc3RhdGlvbl9pZCwgc3RhdGlvbl9pZCwgeWVhciwgRlVOPWxlbmd0aCkpCgptZXJnZV9yYWlsCmBgYAoKCmBgYHtyfQphZ2dfY3N2IDwtIGFnZ3JlZ2F0ZShyaWRlcy50b3RhbCB+IHllYXIgKyByYWlsX2xpbmUsIG1lcmdlX3JhaWwsIHN1bSkKYWdnX2NzdiA8LSB3aXRoKGFnZ19jc3YsIGFnZ19jc3Zbb3JkZXIocmFpbF9saW5lLCB5ZWFyKSxdKQpyb3cubmFtZXMoYWdnX2NzdikgPC0gTlVMTAoKYWdnX2NzdgpgYGAKCiMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5TUUwgRGF0YSBEaWFnbm9zdGljczwvc3Bhbj4KCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5Db21wbGV4IHByb2Nlc3Npbmcgc3RpbGwgcmVhZGFibGUgYW5kIG1haW50YWluYWJsZTwvc3Bhbj4gIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5DVEVzIGNsZWFybHkgc2hvdyB1bmRlcmx5aW5nIHRhYmxlcyBhbmQgdmlld3Mgd2l0aG91dCBoZWxwZXIgb2JqZWN0czwvc3Bhbj4gIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5XaW5kb3cgZnVuY3Rpb25zIGFsbG93IHVzZWZ1bCBpbmxpbmUgY2FsY3VsYXRpb25zPC9zcGFuPiAjIyMKLSAjIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPkNvbmRpdGlvbmFsIGFnZ3JlZ2F0aW9ucyBjbGVhcmx5IHNob3cgcmVzaGFwZWQgZGF0YTwvc3Bhbj4gIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5TZWFtbGVzcyBjYXN0aW5nIGFuZCBjb252ZXJzaW9uIG9mIHR5cGVzIHdpdGggYDo6YCBvcGVyYXRvcjwvc3Bhbj4gIyMjCgo8ZGl2IGNsYXNzPSJibHVlIj4KYGBge3J9CnNxbCA8LSAnV0lUSCBzdGF0aW9uX2FnZyBBUwogICAgICAgICAoU0VMRUNUIERBVEVfUEFSVChcJ3llYXJcJywgci5yaWRlX2RhdGUpOjppbnRlZ2VyIEFTICJ5ZWFyIiwKICAgICAgICAgICAgICAgICByLnN0YXRpb25faWQsCiAgICAgICAgICAgICAgICAgci5zdGF0aW9uX25hbWUsCiAgICAgICAgICAgICAgICAgQ09VTlQoci5yaWRlcyk6Om51bWVyaWMoMjAsNSkgQVMgImNvdW50IiwgCiAgICAgICAgICAgICAgICAgU1VNKHIucmlkZXMpOjpudW1lcmljKDIwLDUpIEFTICJzdW0iLCAKICAgICAgICAgICAgICAgICBBVkcoci5yaWRlcyk6Om51bWVyaWMoMjAsNSkgQVMgIm1lYW4iLCAKICAgICAgICAgICAgICAgICBNRURJQU4oci5yaWRlcyk6Om51bWVyaWMoMjAsNSkgQVMgIm1lZGlhbiIsCiAgICAgICAgICAgICAgICAgTUlOKHIucmlkZXMpOjpudW1lcmljKDIwLDUpIEFTICJtaW4iLCAKICAgICAgICAgICAgICAgICBNQVgoci5yaWRlcyk6Om51bWVyaWMoMjAsNSkgQVMgIm1heCIKICAgICAgICAgIEZST00gcmFpbF9yaWRlcyByCiAgICAgICAgICBHUk9VUCBCWSBEQVRFX1BBUlQoXCd5ZWFyXCcsIHIucmlkZV9kYXRlKSwKICAgICAgICAgICAgICAgICAgIHIuc3RhdGlvbl9pZCwKICAgICAgICAgICAgICAgICAgIHIuc3RhdGlvbl9uYW1lCiAgICAgICAgICApLAogICAgICAgICAgICAgICAgICAgCiAgICAgIG1lcmdlX3JhaWwgQVMKICAgICAgICAgKFNFTEVDVCBzLiosIAogICAgICAgICAgICAgICAgIHIucmFpbF9saW5lLAogICAgICAgICAgICAgICAgIChzLiJzdW0iIC8gQ09VTlQoKikgT1ZFUihQQVJUSVRJT04gQlkgcy5zdGF0aW9uX2lkLCAieWVhciIpKSBBUyByYWlsX3RvdGFsCiAgICAgICAgICBGUk9NIHN0YXRpb25fYWdnIHMKICAgICAgICAgIElOTkVSIEpPSU4gcmFpbF9zdGF0aW9ucyByIE9OIHMuc3RhdGlvbl9pZCA9IHIuc3RhdGlvbl9pZAogICAgICAgICApCiAgICAgICAgIAogICAgICBTRUxFQ1QgbS4ieWVhciIsIG0ucmFpbF9saW5lLCBTVU0obS5yYWlsX3RvdGFsKSAgQVMgcmFpbF90b3RhbAogICAgICBGUk9NIG1lcmdlX3JhaWwgbQogICAgICBHUk9VUCBCWSBtLiJ5ZWFyIiwgbS5yYWlsX2xpbmUKICAgICAgT1JERVIgQlkgbS5yYWlsX2xpbmUsIG0uInllYXIiJwogIAphZ2dfc3FsIDwtIGRiR2V0UXVlcnkoY29ubiwgc3FsKQoKYWdnX3NxbApgYGAKCgpgYGB7ciBmaWczLCBmaWcuaGVpZ2h0ID0gNCwgZmlnLndpZHRoID0gMTAsIGZpZy5hbGlnbiA9ICJjZW50ZXIifQpjdGFfcGFsZXR0ZSA8LSBjKGJsdWU9IiMwMEExREUiLCBicm93bj0iIzYyMzYxQiIsIGdyZWVuPSIjMDA5QjNBIiwgb3JhbmdlPSIjRjk0NjFDIiwgcGluaz0iI0UyN0VBNiIsCiAgICAgICAgICAgICAgICAgcHVycGxlPSIjNTIyMzk4IiwgcHVycGxlX2V4cD0iIzgwNTlCQSIsIHJlZD0iI0M2MEMzMCIsIHllbGxvdz0iI0Y5RTMwMCIpCgpnZ3Bsb3Qoc3Vic2V0KGFnZ19zcWwsIHllYXIgPiAyMDEyKSwgYWVzKHllYXIsIHJhaWxfdG90YWwsIGZpbGw9cmFpbF9saW5lKSkgKyBnZW9tX2NvbChwb3NpdGlvbiA9ICJkb2RnZSIpICsKICBsYWJzKHRpdGxlPSJDVEEgU3lzdGVtICdMJyBMaW5lcyAtIFRvdGFsIFJpZGVzIEJ5IFllYXIiLCB4PSJZZWFyIiwgeT0iUmlkZXMiKSArCiAgc2NhbGVfeF9jb250aW51b3VzKGV4cGFuZCA9IGMoMCwwKSwgInllYXIiLCBicmVha3M9dW5pcXVlKGFnZ19zcWwkeWVhcikpICsKICBzY2FsZV95X2NvbnRpbnVvdXMoZXhwYW5kID0gYygwLCAwKSwgbGFiZWw9Y29tbWEpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBjdGFfcGFsZXR0ZSkgKyBndWlkZXMoZmlsbD1ndWlkZV9sZWdlbmQoIlJhaWwgTGluZXMiLCBucm93PTEpKSArCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uPSJib3R0b20iLAogICAgICAgIHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3Q9MC41LCBzaXplPTE4KSwKICAgICAgICBheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZT0wLCBoanVzdD0wLjUpKQpgYGAKCjwvZGl2PgoKIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTE7Ij5EaXN0cmlidXRpb248L3NwYW4KCmBgYHtyfQpzcWwgPC0gJ1NFTEVDVCByLnN0YXRpb25faWQsIHIucmlkZV9kYXRlLCByLnN0YXRpb25fbmFtZSwgcy5yYWlsX2xpbmUsIHIucmlkZXMsCiAgICAgICAgICAgICAgIChyLnJpZGVzIC8gQ09VTlQoKikgT1ZFUihQQVJUSVRJT04gQlkgcy5zdGF0aW9uX2lkLCByLnJpZGVfZGF0ZSkpIEFTIHJpZGVzX3RvdGFsCiAgICAgICAgRlJPTSByYWlsX3JpZGVzIHIKICAgICAgICBJTk5FUiBKT0lOIHJhaWxfc3RhdGlvbnMgcyBPTiBzLnN0YXRpb25faWQgPSByLnN0YXRpb25faWQnCgpoaXN0X2xvbmcgPC0gZGJHZXRRdWVyeShjb25uLCBzcWwpCgpoaXN0X2xvbmcKYGBgCgpgYGB7ciBmaWc0LCBmaWcuaGVpZ2h0ID0gNCwgZmlnLndpZHRoID0gMTAsIGZpZy5hbGlnbiA9ICJjZW50ZXIifQpnZ3Bsb3QoaGlzdF9sb25nLCBhZXMoeD1yaWRlc190b3RhbCwgZmlsbD1yYWlsX2xpbmUpKSArCiAgIGdlb21faGlzdG9ncmFtKGRhdGE9c3Vic2V0KGhpc3RfbG9uZywgcmFpbF9saW5lID09ICdyZWQnKSwgYmlucz0xMDApICsKICAgZ2VvbV9oaXN0b2dyYW0oZGF0YT1zdWJzZXQoaGlzdF9sb25nLCByYWlsX2xpbmUgPT0gJ2JsdWUnKSwgYmlucz0xMDApICsKICAgZ2VvbV9oaXN0b2dyYW0oZGF0YT1zdWJzZXQoaGlzdF9sb25nLCByYWlsX2xpbmUgPT0gJ2Jyb3duJyksIGJpbnM9MTAwKSArCiAgIGdlb21faGlzdG9ncmFtKGRhdGE9c3Vic2V0KGhpc3RfbG9uZywgcmFpbF9saW5lID09ICdncmVlbicpLCBiaW5zPTEwMCkgKwogICBnZW9tX2hpc3RvZ3JhbShkYXRhPXN1YnNldChoaXN0X2xvbmcsIHJhaWxfbGluZSA9PSAnb3JhbmdlJyksIGJpbnM9MTAwKSArCiAgIHNjYWxlX3hfY29udGludW91cyhleHBhbmQgPSBjKDAsIDApKSArCiAgIHNjYWxlX3lfY29udGludW91cyhleHBhbmQgPSBjKDAsIDApLCBsaW09YygwLDQwMDAwKSwgbGFiZWw9Y29tbWEpICsKICAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gYyhyZWQgPSAiI0M2MEMzMCIsIGJsdWUgPSAiIzAwQTFERSIsIGJyb3duID0gIiM2MjM2MUIiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGdyZWVuID0gIiMwMDlCM0EiLCBvcmFuZ2UgPSAiI0Y5NDYxQyIpKSArCiAgIGxhYnModGl0bGU9IkNUQSBSaWRlcnNoaXAgRGlzdHJpYnV0aW9uIEJ5IFJhaWwgTGluZSIsIGZpbGw9IlJhaWwgTGluZSIpICsKICAgdGhlbWUocGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdD0wLjUsIHNpemU9MTgpKQpgYGAKCmBgYHtyIGZpZzUsIGZpZy5oZWlnaHQgPSA0LCBmaWcud2lkdGggPSAxMCwgZmlnLmFsaWduID0gImNlbnRlciJ9CmdncGxvdCh0cmFuc2Zvcm0oaGlzdF9sb25nLCB5ZWFyID0gZm9ybWF0KHJpZGVfZGF0ZSwgJyVZJykpLCAKICAgICAgIGFlcyh4PXllYXIsIHk9cmlkZXNfdG90YWwsIGZpbGw9eWVhcikpICsgCiAgICBnZW9tX2JveHBsb3QoKSArIGd1aWRlcyhmaWxsPUZBTFNFKSArCiAgICBzY2FsZV95X2NvbnRpbnVvdXMoZXhwYW5kID0gYygwLCAwKSwgbGltPWMoMCw0MDAwMCksIGxhYmVsPWNvbW1hKSArCiAgICBsYWJzKHRpdGxlPSJDVEEgUmlkZXJzaGlwIEJveHBsb3QgQnkgWWVhciIpICsKICAgIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3Q9MC41LCBzaXplPTE4KSkKYGBgCgpgYGB7ciBmaWc2LCBmaWcuaGVpZ2h0ID0gNCwgZmlnLndpZHRoID0gMTAsIGZpZy5hbGlnbiA9ICJjZW50ZXIifQpnZ3Bsb3QoaGlzdF9sb25nLCBhZXMoeD1yYWlsX2xpbmUsIHk9cmlkZXNfdG90YWwsIGZpbGw9cmFpbF9saW5lKSkgKyAKICAgIGdlb21fYm94cGxvdCgpICsgZ3VpZGVzKGZpbGw9RkFMU0UpICsKICAgIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGN0YV9wYWxldHRlKSArCiAgICBzY2FsZV95X2NvbnRpbnVvdXMoZXhwYW5kID0gYygwLCAwKSwgbGltPWMoMCw0MDAwMCksIGxhYmVsPWNvbW1hKSArCiAgICBsYWJzKHRpdGxlPSJDVEEgUmlkZXJzaGlwIEJveHBsb3QgQnkgUmFpbCBMaW5lIikgKwogICAgdGhlbWUocGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdD0wLjUsIHNpemU9MTgpKQpgYGAKCmBgYHtyIGZpZzcsIGZpZy5oZWlnaHQgPSA0LCBmaWcud2lkdGggPSAxMCwgZmlnLmFsaWduID0gImNlbnRlciJ9CmdncGxvdChzdWJzZXQod2l0aGluKGhpc3RfbG9uZywgeyB5ZWFyIDwtIGZvcm1hdChyaWRlX2RhdGUsICclWScpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB5ZWFyIDwtIGFzLmludGVnZXIoYXMuY2hhcmFjdGVyKHllYXIpKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0pLAogICAgICAgICAgICAgIHllYXIgPj0gMjAxNSAmIHllYXIgPD0gMjAxNyksIAogICAgICAgYWVzKHg9ZmFjdG9yKHllYXIpLCB5PXJpZGVzX3RvdGFsLCBmaWxsPXJhaWxfbGluZSkpICsgCiAgICBnZW9tX2JveHBsb3QoKSArIGd1aWRlcyhmaWxsPWd1aWRlX2xlZ2VuZCgiUmFpbCBMaW5lcyIsIG5yb3c9MSkpICsKICAgIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGN0YV9wYWxldHRlKSArCiAgICBzY2FsZV94X2Rpc2NyZXRlKGV4cGFuZCA9IGMoMCwgMCkpICsKICAgIHNjYWxlX3lfY29udGludW91cyhleHBhbmQgPSBjKDAsIDApLCBsaW09YygwLDQwMDAwKSwgbGFiZWw9Y29tbWEpICsKICAgIGxhYnModGl0bGU9IkNUQSBSaWRlcnNoaXAgQm94cGxvdCBCeSBSYWlsIExpbmUsIDIwMTUtMjAxNyIsIHg9IlllYXIiLCB5PSJSaWRlcyIpICsKICAgIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbj0iYm90dG9tIiwgcGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdD0wLjUsIHNpemU9MTgpKQpgYGAKCgojIyA8c3BhbiBzdHlsZT0iY29sb3I6IzMzNjc5MSI+Q29ycmVsYXRpb248L3NwYW4+Cgo8ZGl2IGNsYXNzPSJibHVlIj4KYGBge3J9CnNxbCA8LSAnV0lUSCBzdGF0aW9uX2FnZyBBUwogICAgICAgICAoU0VMRUNUIERBVEVfUEFSVChcJ3llYXJcJywgci5yaWRlX2RhdGUpOjppbnRlZ2VyIEFTICJ5ZWFyIiwKICAgICAgICAgICAgICAgICByLnN0YXRpb25faWQsCiAgICAgICAgICAgICAgICAgci5zdGF0aW9uX25hbWUsCiAgICAgICAgICAgICAgICAgQ09VTlQoci5yaWRlcyk6Om51bWVyaWMoMjAsNSkgQVMgImNvdW50IiwgCiAgICAgICAgICAgICAgICAgU1VNKHIucmlkZXMpOjpudW1lcmljKDIwLDUpIEFTICJzdW0iLCAKICAgICAgICAgICAgICAgICBBVkcoci5yaWRlcyk6Om51bWVyaWMoMjAsNSkgQVMgIm1lYW4iLCAKICAgICAgICAgICAgICAgICBNRURJQU4oci5yaWRlcyk6Om51bWVyaWMoMjAsNSkgQVMgIm1lZGlhbiIsCiAgICAgICAgICAgICAgICAgTUlOKHIucmlkZXMpOjpudW1lcmljKDIwLDUpIEFTICJtaW4iLCAKICAgICAgICAgICAgICAgICBNQVgoci5yaWRlcyk6Om51bWVyaWMoMjAsNSkgQVMgIm1heCIKICAgICAgICAgIEZST00gcmFpbF9yaWRlcyByCiAgICAgICAgICBHUk9VUCBCWSBEQVRFX1BBUlQoXCd5ZWFyXCcsIHIucmlkZV9kYXRlKSwKICAgICAgICAgICAgICAgICAgIHIuc3RhdGlvbl9pZCwKICAgICAgICAgICAgICAgICAgIHIuc3RhdGlvbl9uYW1lCiAgICAgICAgICApLAogICAgICAgICAgICAgICAgICAgCiAgICAgIG1lcmdlX3JhaWwgQVMKICAgICAgICAgKFNFTEVDVCBzLiosIAogICAgICAgICAgICAgICAgIHIucmFpbF9saW5lLAogICAgICAgICAgICAgICAgIChzLiJzdW0iIC8gQ09VTlQoKikgT1ZFUihQQVJUSVRJT04gQlkgcy5zdGF0aW9uX2lkLCAieWVhciIpKSBBUyByYWlsX3RvdGFsCiAgICAgICAgICBGUk9NIHN0YXRpb25fYWdnIHMKICAgICAgICAgIElOTkVSIEpPSU4gcmFpbF9zdGF0aW9ucyByIE9OIHMuc3RhdGlvbl9pZCA9IHIuc3RhdGlvbl9pZAogICAgICAgICApCiAgICAgICAgIAogICAgICBTRUxFQ1QgbS4ieWVhciIsCiAgICAgICAgICAgICBTVU0ocmFpbF90b3RhbCkgRklMVEVSIChXSEVSRSByYWlsX2xpbmUgPSBcJ2JsdWVcJykgQVMgYmx1ZSwKICAgICAgICAgICAgIFNVTShyYWlsX3RvdGFsKSBGSUxURVIgKFdIRVJFIHJhaWxfbGluZSA9IFwnYnJvd25cJykgQVMgYnJvd24sCiAgICAgICAgICAgICBTVU0ocmFpbF90b3RhbCkgRklMVEVSIChXSEVSRSByYWlsX2xpbmUgPSBcJ2dyZWVuXCcpIEFTIGdyZWVuLAogICAgICAgICAgICAgU1VNKHJhaWxfdG90YWwpIEZJTFRFUiAoV0hFUkUgcmFpbF9saW5lID0gXCdvcmFuZ2VcJykgQVMgb3JhbmdlLAogICAgICAgICAgICAgU1VNKHJhaWxfdG90YWwpIEZJTFRFUiAoV0hFUkUgcmFpbF9saW5lID0gXCdwaW5rXCcpIEFTIHBpbmssCiAgICAgICAgICAgICBTVU0ocmFpbF90b3RhbCkgRklMVEVSIChXSEVSRSByYWlsX2xpbmUgPSBcJ3B1cnBsZVwnKSBBUyBwdXJwbGUsCiAgICAgICAgICAgICBTVU0ocmFpbF90b3RhbCkgRklMVEVSIChXSEVSRSByYWlsX2xpbmUgPSBcJ3B1cnBsZV9leHBcJykgQVMgcHVycGxlX2V4cCwKICAgICAgICAgICAgIFNVTShyYWlsX3RvdGFsKSBGSUxURVIgKFdIRVJFIHJhaWxfbGluZSA9IFwncmVkXCcpIEFTIHJlZCwKICAgICAgICAgICAgIFNVTShyYWlsX3RvdGFsKSBGSUxURVIgKFdIRVJFIHJhaWxfbGluZSA9IFwneWVsbG93XCcpIEFTIHllbGxvdwogICAgICBGUk9NIG1lcmdlX3JhaWwgbQogICAgICBHUk9VUCBCWSBtLiJ5ZWFyIgogICAgICBPUkRFUiBCWSBtLiJ5ZWFyIicKCndpZGVfc3FsIDwtIGRiR2V0UXVlcnkoY29ubiwgc3FsKQoKd2lkZV9zcWwKYGBgCjwvZGl2PgoKPGRpdiBjbGFzcz0iYmx1ZSI+CmBgYHtyfQpjb3Iod2lkZV9zcWxbLTFdLCB1c2UgPSAiY29tcGxldGUub2JzIiwgbWV0aG9kPSJwZWFyc29uIikKYGBgCjwvZGl2PgoKIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPlQtdGVzdHM8L3NwYW4+Cgo8ZGl2IGNsYXNzPSJibHVlIj4KYGBge3J9CmNvbWJucyA8LSBhcy5saXN0KG91dGVyKHJhaWxfbGluZXMsIHJhaWxfbGluZXMsIGZ1bmN0aW9uKHgsIHkpIGlmZWxzZSh4PT15LCBOQSwgcGFzdGUoeCwgeSkpKSkKY29tYm5zIDwtIGxhcHBseShjb21ibnNbIWlzLm5hKGNvbWJucyldLCBmdW5jdGlvbih4KSBzdHJzcGxpdCh4LCBzcGxpdD0iICIpW1sxXV0pCgp0dGVzdF9tYXRyaXggPC0gc2FwcGx5KGNvbWJucywgZnVuY3Rpb24oeCl7CiAgdCA8LSB0LnRlc3Qod2lkZV9zcWxbW3hbMV1dXSwgd2lkZV9zcWxbW3hbMl1dXSkKICBjKHN0YXRpc3RpYyA9IHQkc3RhdGlzdGljLCBwX3ZhbHVlID0gdCRwLnZhbHVlKQogIAp9KQoKdHRlc3RfZGYgPC0gZGF0YS5mcmFtZSh4ID0gc2FwcGx5KGNvbWJucywgIlsiLCAxKSwKICAgICAgICAgICAgICAgICAgICAgICB5ID0gc2FwcGx5KGNvbWJucywgIlsiLCAyKSwKICAgICAgICAgICAgICAgICAgICAgICBzdGF0aXN0aWMgPSB0dGVzdF9tYXRyaXhbMSxdLAogICAgICAgICAgICAgICAgICAgICAgIHBfdmFsdWUgPSB0dGVzdF9tYXRyaXhbMixdKQoKdHRlc3RfZGYgPC0gd2l0aCh0dGVzdF9kZiwgdHRlc3RfZGZbb3JkZXIoeCwgeSksXSkKdHRlc3RfZGYKYGBgCgoKYGBge3IgZmlnLmhlaWdodCA9IDQsIGZpZy53aWR0aCA9IDEwLCBmaWcuYWxpZ24gPSAiY2VudGVyIn0KCmJ5KHR0ZXN0X2RmLCB0dGVzdF9kZiR4LCBmdW5jdGlvbihzdWIpCiAgCiAgZ2dwbG90KHN1YiwgYWVzKHgsIHN0YXRpc3RpYywgZmlsbD15KSkgKyBnZW9tX2NvbChwb3NpdGlvbiA9ICJkb2RnZSIpICsKICAgIGxhYnModGl0bGU9cGFzdGUwKCJDVEEgU3lzdGVtICdMJyBMaW5lcyAtIFBhaXJ3aXNlIFQtdGVzdHMgZm9yICIsIAogICAgICAgICAgICAgICAgICAgICB0b3VwcGVyKHN1YnN0cihzdWIkeFtbMV1dLCAxLCAxKSksIAogICAgICAgICAgICAgICAgICAgICBzdWJzdHIoc3ViJHhbWzFdXSwgMiwgbmNoYXIoYXMuY2hhcmFjdGVyKHN1YiR4W1sxXV0pKSksICIgTGluZSIpLCAKICAgICAgICAgeD0iUmFpbCBMaW5lIiwgeT0iVC10ZXN0IFN0YXQiKSArCiAgICBzY2FsZV94X2Rpc2NyZXRlKGV4cGFuZCA9IGMoMCwwKSkgKyAKICAgIHNjYWxlX3lfY29udGludW91cyhleHBhbmQgPSBjKDAsIDApLCBsYWJlbD1jb21tYSkgKwogICAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gY3RhX3BhbGV0dGVbIW5hbWVzKGN0YV9wYWxldHRlKT09c3ViJHhbWzFdXV0pICsgCiAgICBndWlkZXMoZmlsbD1ndWlkZV9sZWdlbmQoIlJhaWwgTGluZXMiLCBucm93PTEpKSArCiAgICB0aGVtZShsZWdlbmQucG9zaXRpb249ImJvdHRvbSIsCiAgICAgICAgICBwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0PTAuNSwgc2l6ZT0xOCwgY29sb3VyPWN0YV9wYWxldHRlW25hbWVzKGN0YV9wYWxldHRlKT09c3ViJHhbWzFdXV0pLAogICAgICAgICAgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGU9MCwgaGp1c3Q9MC41KSkKICApCgpgYGAKPC9kaXY+CgojIyA8c3BhbiBzdHlsZT0iY29sb3I6ICM2NDY0NjQiPkNTViBSZWdyZXNzaW9uPC9zcGFuPiAjIwoKIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+UmlnaHQtSGFuZCBTaWRlIFZhcmlhYmxlczwvc3Bhbj4gIyMKCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+QWdhaW4sIHJlcXVpcmVzIG1haW50ZW5hbmNlIGFuZCBzdG9yYWdlIG9uIGRpc2sgc3BhY2U8L3NwYW4+ICMjIwotICMjIyA8c3BhbiBzdHlsZT0iY29sb3I6ICM2NDY0NjQiPkFnYWluLCByZXF1aXJlcyBsb2FkIHRpbWUgZnJvbSBub24tY2VudHJhbGl6ZWQgcGF0aHM8L3NwYW4+ICMjIwotICMjIyA8c3BhbiBzdHlsZT0iY29sb3I6ICM2NDY0NjQiPkFnYWluLCByZXF1aXJlcyBhbnkgYWQtaG9jIG11bmdpbmcgZm9yIHVzYWJpbGl0eTwvc3Bhbj4gIyMjCgpgYGB7cn0KCiMgU291cmNlOiBTdC4gTG91aXMgRmVkZXJhbCBSZXNlcnZlIEJhbmsKdW5lbXBsb3ltZW50X2RmIDwtIHJlYWQuY3N2KCdDaGljYWdvX1VuZW1wbG95bWVudF9SYXRlcy5jc3YnKQp1bmVtcGxveW1lbnRfZGYkRGF0ZSA8LSBhcy5EYXRlKHVuZW1wbG95bWVudF9kZiREYXRlLCBmb3JtYXQ9JyVZLSVtLSVkJywgb3JpZ2luPScxOTcwLTAxLTAxJykKaGVhZCh1bmVtcGxveW1lbnRfZGYpCgojIFNvdXJjZTogVS5TLiBEZXBhcnRtZW50IG9mIEVuZXJneTogRUlBCmdhc19wcmljZXNfZGYgPC0gcmVhZC5jc3YoJ1VTX0dhc19QcmljZXMuY3N2JykKZ2FzX3ByaWNlc19kZiREYXRlIDwtIGFzLkRhdGUoZ2FzX3ByaWNlc19kZiREYXRlLCBmb3JtYXQ9JyVZLSVtLSVkJywgb3JpZ2luPScxOTcwLTAxLTAxJykKaGVhZChnYXNfcHJpY2VzX2RmKQoKIyBTb3VyY2U6IFUuUy4gTmF0aW9uYWwgT2NlYW5pYyBhbmQgQXRtb3NwaGVyaWMgQWRtaW5pc3RyYXRpb24gKE5PQUEpCndlYXRoZXJfZGYgPC0gcmVhZC5jc3YoJ0NoaWNhZ29fV2VhdGhlcl9EYXRhLmNzdicpCndlYXRoZXJfZGYkRGF0ZSA8LSBhcy5EYXRlKHdlYXRoZXJfZGYkRGF0ZSwgZm9ybWF0PSclWS0lbS0lZCcsIG9yaWdpbj0nMTk3MC0wMS0wMScpCmhlYWQod2VhdGhlcl9kZikKYGBgCgoKIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNjQ2NDY0Ij5CdXMgTW9kZWwgRGF0YTwvc3Bhbj4KCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+RWNob2VzIHNldC1iYXNlZCBqb2luczwvc3Bhbj4gIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+UmVwZXRpdGl2ZSBzb3VyY2luZyAgb2Ygc2FtZSBvYmplY3Q8L3NwYW4+ICMjIwotICMjIyA8c3BhbiBzdHlsZT0iY29sb3I6ICM2NDY0NjQiPk5lc3RlZCBkZW5zZSBwcm9jZXNzaW5nIG9mIHN0ZXBzPC9zcGFuPiAjIyMKCmBgYHtyfQpidXNfbW9kZWxfZGF0YSA8LSBtZXJnZSh1bmlxdWUoYnVzX21vbnRoX2NzdltjKCJyb3V0ZSIsICJyb3V0ZW5hbWUiKV0pLCBidXNfcmlkZXNfY3N2LCBieT0icm91dGUiKQoKYnVzX21vZGVsX2RhdGEgPC0gbWVyZ2UoYnVzX21vZGVsX2RhdGEsIHVuZW1wbG95bWVudF9kZiwgYnkueD0nZGF0ZScsIGJ5Lnk9J0RhdGUnKQpidXNfbW9kZWxfZGF0YSA8LSBtZXJnZShidXNfbW9kZWxfZGF0YSwgZ2FzX3ByaWNlc19kZiwgYnkueD0nZGF0ZScsIGJ5Lnk9J0RhdGUnKQpidXNfbW9kZWxfZGF0YSA8LSBtZXJnZShidXNfbW9kZWxfZGF0YSwgd2VhdGhlcl9kZiwgYnkueD0nZGF0ZScsIGJ5Lnk9J0RhdGUnKQoKaGVhZChidXNfbW9kZWxfZGF0YSkKYGBgCgoKIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+QWRkIFNlYXNvbnM8L3NwYW4+CgpgYGB7cn0KYnVzX21vZGVsX2RhdGEkbm9ybWFsaXplZF9kdCA8LSBhcy5QT1NJWGx0KGJ1c19tb2RlbF9kYXRhJGRhdGUpCmJ1c19tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQkeWVhciA8LSBidXNfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0JHllYXIgKwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKDIwOTkgLSBhcy5pbnRlZ2VyKGZvcm1hdChidXNfbW9kZWxfZGF0YSRkYXRlLCAiJVkiKSkpCmJ1c19tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQgPC0gYXMuRGF0ZShidXNfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0KQoKCmJ1c19tb2RlbF9kYXRhJHNlYXNvbiA8LSBpZmVsc2UoYnVzX21vZGVsX2RhdGEkbm9ybWFsaXplZF9kdCA+PSAnMjA5OS0wMS0wMScgJiAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBidXNfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0ICA8ICcyMDk5LTAzLTE5JywgJ3dpbnRlcicsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaWZlbHNlKGJ1c19tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQgPj0gJzIwOTktMDMtMjAnICYgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJ1c19tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQgIDwgJzIwOTktMDYtMTknLCAnc3ByaW5nJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaWZlbHNlKGJ1c19tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQgPj0gJzIwOTktMDYtMjAnICYgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBidXNfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0ICA8ICcyMDk5LTA5LTE5JywgJ3N1bW1lcicsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpZmVsc2UoYnVzX21vZGVsX2RhdGEkbm9ybWFsaXplZF9kdCA+PSAnMjA5OS0wOS0yMCcgJiAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBidXNfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0ICA8ICcyMDk5LTEyLTE5JywgJ2ZhbGwnLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGlmZWxzZShidXNfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0ID49ICcyMDk5LTEyLTIwJyAmIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJ1c19tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQgIDwgJzIwOTktMTItMzEnLCAnd2ludGVyJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgTkEpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICApCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICkKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICApCiAgICAgICAgICAgICAgICAgICAgICAgICApCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIApidXNfbW9kZWxfZGF0YVtzYW1wbGUobnJvdyhidXNfbW9kZWxfZGF0YSksIDEwKSwgYygibm9ybWFsaXplZF9kdCIsICJkYXRlIiwgInNlYXNvbiIpXQpidXNfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0IDwtIE5VTEwKYGBgCgojIyA8c3BhbiBzdHlsZT0iY29sb3I6ICM2NDY0NjQiPkJ1cyBNb2RlbGluZyAoT3JkaW5hcnkgTGVhc3QgU3F1YXJlcyk8L3NwYW4+CgpgYGB7cn0KbW9kZWwgPC0gbG0ocmlkZXMgfiBkYXl0eXBlICsgc2Vhc29uICsgVUVfUmF0ZSArIEdhc19QcmljZSArIEF2Z1RlbXAgKyBQcmVjaXBpdGF0aW9uICsgU25vd0RlcHRoLAogICAgICAgICAgICBkYXRhID0gYnVzX21vZGVsX2RhdGEpCgpzdW1tYXJ5KG1vZGVsKQpgYGAKCiMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+UmFpbCBNb2RlbCBEYXRhPC9zcGFuPgoKIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogIzY0NjQ2NCI+QXNzaWduIExhdGl0dWRlIGFuZCBMb25naXR1ZGU8L3NwYW4+CgpgYGB7cn0KCnJhaWxfbG9uZyRsYXRpdHVkZSA8LSBhcy5udW1lcmljKGdzdWIoIlxcKCIsICIiLCBnc3ViKCIsIiwgIiIsIHNhcHBseShhcy5jaGFyYWN0ZXIocmFpbF9sb25nJGxvY2F0aW9uKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGZ1bmN0aW9uKHgpIHN0cnNwbGl0KHgsIHNwbGl0PSJcXHMrIilbWzFdXVsxXSkpKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICkKCnJhaWxfbG9uZyRsb25naXR1ZGUgPC0gYXMubnVtZXJpYyhnc3ViKCJcXCkiLCAiIiwgc2FwcGx5KGFzLmNoYXJhY3RlcihyYWlsX2xvbmckbG9jYXRpb24pLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmdW5jdGlvbih4KSBzdHJzcGxpdCh4LCBzcGxpdD0iXFxzKyIpW1sxXV1bMl0pKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKQoKcmFpbF9sb25nW3NhbXBsZShucm93KHJhaWxfbG9uZyksIDEwKSwgYygibG9jYXRpb24iLCAibGF0aXR1ZGUiLCAibG9uZ2l0dWRlIildCmBgYAoKYGBge3J9CnJhaWxfbW9kZWxfZGF0YSA8LSBtZXJnZShyYWlsX2xvbmcsIHJhaWxfcmlkZXNfY3N2LCBieT0ic3RhdGlvbl9pZCIpCgpyYWlsX21vZGVsX2RhdGEkcmF3IDwtIHJhaWxfbW9kZWxfZGF0YSRyaWRlcwoKcmFpbF9tb2RlbF9kYXRhJHJpZGVzIDwtIHdpdGgocmFpbF9tb2RlbF9kYXRhLCByaWRlcyAvCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhdmUoc3RhdGlvbl9pZCwgc3RhdGlvbl9pZCwgZGF0ZSwgRlVOPWxlbmd0aCkpCgpyYWlsX21vZGVsX2RhdGEgPC0gbWVyZ2UocmFpbF9tb2RlbF9kYXRhLCB1bmVtcGxveW1lbnRfZGYsIGJ5Lng9J2RhdGUnLCBieS55PSdEYXRlJykKcmFpbF9tb2RlbF9kYXRhIDwtIG1lcmdlKHJhaWxfbW9kZWxfZGF0YSwgZ2FzX3ByaWNlc19kZiwgYnkueD0nZGF0ZScsIGJ5Lnk9J0RhdGUnKQpyYWlsX21vZGVsX2RhdGEgPC0gbWVyZ2UocmFpbF9tb2RlbF9kYXRhLCB3ZWF0aGVyX2RmLCBieS54PSdkYXRlJywgYnkueT0nRGF0ZScpCgpoZWFkKHJhaWxfbW9kZWxfZGF0YSwgMTApCmBgYAoKCiMjIyA8c3BhbiBzdHlsZT0iY29sb3I6ICM2NDY0NjQiPkFkZCBTZWFzb25zPC9zcGFuPgoKYGBge3J9CnJhaWxfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0IDwtIGFzLlBPU0lYbHQocmFpbF9tb2RlbF9kYXRhJGRhdGUpCnJhaWxfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0JHllYXIgPC0gcmFpbF9tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQkeWVhciArCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAoMjA5OSAtIGFzLmludGVnZXIoZm9ybWF0KHJhaWxfbW9kZWxfZGF0YSRkYXRlLCAiJVkiKSkpCnJhaWxfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0IDwtIGFzLkRhdGUocmFpbF9tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQpCgoKcmFpbF9tb2RlbF9kYXRhJHNlYXNvbiA8LSBpZmVsc2UocmFpbF9tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQgPj0gJzIwOTktMDEtMDEnICYgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFpbF9tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQgIDwgJzIwOTktMDMtMTknLCAnd2ludGVyJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpZmVsc2UocmFpbF9tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQgPj0gJzIwOTktMDMtMjAnICYgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJhaWxfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0ICA8ICcyMDk5LTA2LTE5JywgJ3NwcmluZycsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGlmZWxzZShyYWlsX21vZGVsX2RhdGEkbm9ybWFsaXplZF9kdCA+PSAnMjA5OS0wNi0yMCcgJiAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJhaWxfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0ICA8ICcyMDk5LTA5LTE5JywgJ3N1bW1lcicsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpZmVsc2UocmFpbF9tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQgPj0gJzIwOTktMDktMjAnICYgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFpbF9tb2RlbF9kYXRhJG5vcm1hbGl6ZWRfZHQgIDwgJzIwOTktMTItMTknLCAnZmFsbCcsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaWZlbHNlKHJhaWxfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0ID49ICcyMDk5LTEyLTIwJyAmIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJhaWxfbW9kZWxfZGF0YSRub3JtYWxpemVkX2R0ICA8ICcyMDk5LTEyLTMxJywgJ3dpbnRlcicsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIE5BKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICApCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKQogICAgICAgICAgICAgICAgICAgICAgICAgKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKcmFpbF9tb2RlbF9kYXRhW3NhbXBsZShucm93KHJhaWxfbW9kZWxfZGF0YSksIDEwKSwgYygibm9ybWFsaXplZF9kdCIsICJkYXRlIiwgInNlYXNvbiIpXQpyYWlsX21vZGVsX2RhdGEkbm9ybWFsaXplZF9kdCA8LSBOVUxMCgpgYGAKCgoKIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNjQ2NDY0Ij5SYWlsIE1vZGVsaW5nIChPcmRpbmFyeSBMZWFzdCBTcXVhcmVzKTwvc3Bhbj4KCmBgYHtyfQptb2RlbCA8LSBsbShyaWRlcyB+IGRheXR5cGUgKyBzZWFzb24gKyBsYXRpdHVkZSArIGxvbmdpdHVkZSArIHJhaWxfbGluZSArIAogICAgICAgICAgICAgICAgICAgIFVFX1JhdGUgKyBHYXNfUHJpY2UgKyBBdmdUZW1wICsgUHJlY2lwaXRhdGlvbiArIFNub3dEZXB0aCwgCiAgICAgICAgICAgIGRhdGEgPSByYWlsX21vZGVsX2RhdGEpCgpzdW1tYXJ5KG1vZGVsKQpgYGAKCiMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5TUUwgUmVncmVzc2lvbjwvc3Bhbj4KCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5BZHZhbmNlZCBwcmVwYXJhdGlvbiBvZiBkYXRhPC9zcGFuPiAjIyMKLSAjIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPk1hdGVyaWFsaXplZCB2aWV3IGZhY2lsaXRhdGVzIHJlcHJvZHVjaWJsZSByZXNlYXJjaDwvc3Bhbj4gIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5Db21wYWN0IGFuZCBzdHJhaWdodGZvcndhcmQgZGF0YSBzb3VyY2luZzwvc3Bhbj4gIyMjCgojIyA8c3BhbiBzdHlsZT0iY29sb3I6IzMzNjc5MSI+QnVzIE1vZGVsaW5nIERhdGE8L3NwYW4+Cgo8ZGl2IGNsYXNzPSJibHVlIj4KYGBge3NxbCwgZXZhbD1GQUxTRX0KQ1JFQVRFIE1BVEVSSUFMSVpFRCBWSUVXIEJ1c19Nb2RlbF9EYXRhIEFTCiAgICBTRUxFQ1QgYi5pZCwgYi5yb3V0ZV9pZCwgYi5yaWRlX2RhdGUsIGIuZGF5X3R5cGUsIGIucmlkZXMsIHIucm91dGVfbmFtZSwgCiAgICAgICAgICAgQ0FTRSAKICAgICAgICAgICAgICAgV0hFTiBiLm5vcm1hbGl6ZWRfZGF0ZSBCRVRXRUVOICcyMDk5LTAxLTAxJyBBTkQgJzIwOTktMDMtMTknIFRIRU4gJ3dpbnRlcicKICAgICAgICAgICAgICAgV0hFTiBiLm5vcm1hbGl6ZWRfZGF0ZSBCRVRXRUVOICcyMDk5LTAzLTIwJyBBTkQgJzIwOTktMDYtMTknIFRIRU4gJ3NwcmluZycKICAgICAgICAgICAgICAgV0hFTiBiLm5vcm1hbGl6ZWRfZGF0ZSBCRVRXRUVOICcyMDk5LTA2LTIwJyBBTkQgJzIwOTktMDktMTknIFRIRU4gJ3N1bW1lcicKICAgICAgICAgICAgICAgV0hFTiBiLm5vcm1hbGl6ZWRfZGF0ZSBCRVRXRUVOICcyMDk5LTA5LTIwJyBBTkQgJzIwOTktMTItMTknIFRIRU4gJ2ZhbGwnCiAgICAgICAgICAgICAgIFdIRU4gYi5ub3JtYWxpemVkX2RhdGUgQkVUV0VFTiAnMjA5OS0xMi0yMCcgQU5EICcyMDk5LTEyLTMxJyBUSEVOICd3aW50ZXInCiAgICAgICAgICAgICAgIEVMU0UgTlVMTAogICAgICAgICAgIEVORCBBcyBzZWFzb24sCiAgICAgICAgICAgdWUudWVfcmF0ZSwgZy5nYXNfcHJpY2UsIHcuYXZnX3RlbXAsIHcucHJlY2lwaXRhdGlvbiwgdy5zbm93X2RlcHRoCiAgICBGUk9NIAogICAgICgKICAgICAgU0VMRUNUIGlkLCByb3V0ZV9pZCwgZGF5X3R5cGUsIHJpZGVzLCByaWRlX2RhdGUsIAogICAgICAgICAgICAgcmlkZV9kYXRlICsgKDIwOTkgLSBkYXRlX3BhcnQoJ3llYXInLCByaWRlX2RhdGUpICB8fCcgeWVhcicpOjppbnRlcnZhbCBhcyBub3JtYWxpemVkX2RhdGUKICAgICAgRlJPTSBidXNfcmlkZXMKICAgICApIGIKICAgIElOTkVSIEpPSU4gYnVzX3JvdXRlcyByIE9OIGIucm91dGVfaWQgPSByLnJvdXRlX2lkCiAgICBJTk5FUiBKT0lOIHVuZW1wbG95bWVudF9yYXRlcyB1ZSBPTiB1ZS51ZV9kYXRlID0gYi5yaWRlX2RhdGUKICAgIElOTkVSIEpPSU4gZ2FzX3ByaWNlcyBnIE9OIGcuZ2FzX2RhdGUgPSBiLnJpZGVfZGF0ZQogICAgSU5ORVIgSk9JTiB3ZWF0aGVyX2RhdGEgdyBPTiB3LndlYXRoZXJfZGF0ZSA9IGIucmlkZV9kYXRlCiAgICBPUkRFUiBCWSBiLnJpZGVfZGF0ZSwgTlVMTElGKHJlZ2V4cF9yZXBsYWNlKGIucm91dGVfaWQsICdcRCcsICcnLCAnZycpLCAnJyk6OmludDsKICAgIApSRUZSRVNIIE1BVEVSSUFMSVpFRCBWSUVXIEJ1c19Nb2RlbF9EYXRhOwpgYGAKCmBgYHtyfQpidXNfbW9kZWxfZGF0YSA8LSBkYkdldFF1ZXJ5KGNvbm4sICJTRUxFQ1QgKiBGUk9NIGJ1c19tb2RlbF9kYXRhIikKCmhlYWQoYnVzX21vZGVsX2RhdGEpCmBgYAo8L2Rpdj4KCiMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5CdXMgTW9kZWxpbmcgKE9yZGluYXJ5IExlYXN0IFNxdWFyZXMpPC9zcGFuPgoKPGRpdiBjbGFzcz0iYmx1ZSI+CmBgYHtyfQptb2RlbCA8LSBsbShyaWRlcyB+IGRheV90eXBlICsgc2Vhc29uICsgdWVfcmF0ZSArIGdhc19wcmljZSArIGF2Z190ZW1wICsgcHJlY2lwaXRhdGlvbiArIHNub3dfZGVwdGgsCiAgICAgICAgICAgIGRhdGEgPSBidXNfbW9kZWxfZGF0YSkKCnN1bW1hcnkobW9kZWwpCmBgYAo8L2Rpdj4KCiMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5SYWlsIE1vZGVsaW5nIERhdGE8L3NwYW4+Cgo8ZGl2IGNsYXNzPSJibHVlIj4KYGBge3NxbCwgZXZhbD1GQUxTRX0KQ1JFQVRFIE1BVEVSSUFMSVpFRCBWSUVXIFJhaWxfTW9kZWxfRGF0YSBBUwogICAgU0VMRUNUIHIuaWQsIHIuc3RhdGlvbl9pZCwgci5zdGF0aW9uX25hbWUsIHIucmlkZV9kYXRlLCByLmRheV90eXBlLCByLnJpZGVzIEFTIHJhdywgCiAgICAgICAgICAoci5yaWRlcyAvIENPVU5UKCopIE9WRVIoUEFSVElUSU9OIEJZIHIuc3RhdGlvbl9pZCwgci5yaWRlX2RhdGUpKSBBUyByaWRlcywKICAgICAgICAgIENBU0UgCiAgICAgICAgICAgICAgIFdIRU4gci5ub3JtYWxpemVkX2RhdGUgQkVUV0VFTiAnMjA5OS0wMS0wMScgQU5EICcyMDk5LTAzLTE5JyBUSEVOICd3aW50ZXInCiAgICAgICAgICAgICAgIFdIRU4gci5ub3JtYWxpemVkX2RhdGUgQkVUV0VFTiAnMjA5OS0wMy0yMCcgQU5EICcyMDk5LTA2LTE5JyBUSEVOICdzcHJpbmcnCiAgICAgICAgICAgICAgIFdIRU4gci5ub3JtYWxpemVkX2RhdGUgQkVUV0VFTiAnMjA5OS0wNi0yMCcgQU5EICcyMDk5LTA5LTE5JyBUSEVOICdzdW1tZXInCiAgICAgICAgICAgICAgIFdIRU4gci5ub3JtYWxpemVkX2RhdGUgQkVUV0VFTiAnMjA5OS0wOS0yMCcgQU5EICcyMDk5LTEyLTE5JyBUSEVOICdmYWxsJwogICAgICAgICAgICAgICBXSEVOIHIubm9ybWFsaXplZF9kYXRlIEJFVFdFRU4gJzIwOTktMTItMjAnIEFORCAnMjA5OS0xMi0zMScgVEhFTiAnd2ludGVyJwogICAgICAgICAgICAgICBFTFNFIE5VTEwKICAgICAgICAgICBFTkQgQXMgc2Vhc29uLCAgICAgICAgCiAgICAgICAgICAgUkVQTEFDRShSRVBMQUNFKChyZWdleHBfc3BsaXRfdG9fYXJyYXkocy5sb2NhdGlvbiwgJ1xzKycpKVsxXSwgJywnLCAnJyksICcoJywgJycpOjpudW1lcmljIEFTIGxhdGl0dWRlLAogICAgICAgICAgIFJFUExBQ0UoKHJlZ2V4cF9zcGxpdF90b19hcnJheShzLmxvY2F0aW9uLCAnXHMrJykpWzJdLCAnKScsICcnKTo6bnVtZXJpYyBBUyBsb25naXR1ZGUsCiAgICAgICAgICAgcy5yYWlsX2xpbmUsIHMuYWRhLCBzLmRpcmVjdGlvbiwKICAgICAgICAgICB1ZS51ZV9yYXRlLCBnLmdhc19wcmljZSwgdy5hdmdfdGVtcCwgdy5wcmVjaXBpdGF0aW9uLCB3LnNub3dfZGVwdGgKICAgIEZST00gCiAgICAgICAoCiAgICAgICAgU0VMRUNUIGlkLCBzdGF0aW9uX2lkLCBzdGF0aW9uX25hbWUsIGRheV90eXBlLCByaWRlcywgcmlkZV9kYXRlLCAKICAgICAgICAgICAgICAgcmlkZV9kYXRlICsgKDIwOTkgLSBkYXRlX3BhcnQoJ3llYXInLCByaWRlX2RhdGUpICB8fCcgeWVhcicpOjppbnRlcnZhbCBhcyBub3JtYWxpemVkX2RhdGUKICAgICAgICBGUk9NIHJhaWxfcmlkZXMKICAgICAgIClyCiAgICBJTk5FUiBKT0lOIHJhaWxfc3RhdGlvbnMgcyBPTiBzLnN0YXRpb25faWQgPSByLnN0YXRpb25faWQKICAgIElOTkVSIEpPSU4gdW5lbXBsb3ltZW50X3JhdGVzIHVlIE9OIHVlLnVlX2RhdGUgPSByLnJpZGVfZGF0ZQogICAgSU5ORVIgSk9JTiBnYXNfcHJpY2VzIGcgT04gZy5nYXNfZGF0ZSA9IHIucmlkZV9kYXRlCiAgICBJTk5FUiBKT0lOIHdlYXRoZXJfZGF0YSB3IE9OIHcud2VhdGhlcl9kYXRlID0gci5yaWRlX2RhdGUKICAgIE9SREVSIEJZIHIucmlkZV9kYXRlLCByLnN0YXRpb25faWQ7CiAgICAKUkVGUkVTSCBNQVRFUklBTElaRUQgVklFVyBSYWlsX01vZGVsX0RhdGE7CmBgYAoKYGBge3J9CnJhaWxfbW9kZWxfZGF0YSA8LSBkYkdldFF1ZXJ5KGNvbm4sICJTRUxFQ1QgKiBGUk9NIHJhaWxfbW9kZWxfZGF0YSIpCgpoZWFkKHJhaWxfbW9kZWxfZGF0YSkKYGBgCjwvZGl2PgoKIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPlJhaWwgTW9kZWxpbmcgKE9yZGluYXJ5IExlYXN0IFNxdWFyZXMpPC9zcGFuPgoKPGRpdiBjbGFzcz0iYmx1ZSI+CmBgYHtyfQptb2RlbCA8LSBsbShyaWRlcyB+IGRheV90eXBlICsgc2Vhc29uICsgbGF0aXR1ZGUgKyBsb25naXR1ZGUgKyByYWlsX2xpbmUgKyAKICAgICAgICAgICAgICAgICAgICB1ZV9yYXRlICsgZ2FzX3ByaWNlICsgYXZnX3RlbXAgKyBwcmVjaXBpdGF0aW9uICsgc25vd19kZXB0aCwgCiAgICAgICAgICAgIGRhdGEgPSByYWlsX21vZGVsX2RhdGEpCgpzdW1tYXJ5KG1vZGVsKQpgYGAKPC9kaXY+CgpgYGB7cn0KIyBESVNDT05ORUNUIEZST00gREFUQUJBU0UKZGJEaXNjb25uZWN0KGNvbm4pCmBgYAoKIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPkNvbmNsdXNpb25zPC9zcGFuPiAjIwoKLSAjIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPlBvc3RncmVzIHByb3ZpZGVzIGEgc3RhYmxlLCBjZW50cmFsaXplZCwgcmVwb3NpdG9yeSBmb3IgZGF0YSBzb3VyY2luZzwvc3Bhbj4gIyMjCi0gIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjojMzM2NzkxIj5Qb3N0Z3JlcyBtYWludGFpbnMgYW4gZXhwcmVzc2l2ZSBTUUwgZGlhbGVjdCBmb3IgZGF0YSBwcm9jZXNzaW5nPC9zcGFuPiAjIyMKLSAjIyMgPHNwYW4gc3R5bGU9ImNvbG9yOiMzMzY3OTEiPlBvc3RncmVzIHN1cHBvcnRzIGRhdGEgc2NpZW5jZSB3aXRoIHZlY3Rvcml6ZWQgbWV0aG9kcyBhbmQgcmVwcm9kdWNpYmlsaXR5PC9zcGFuPiAjIyMKCgo=